pgo-ui 1.0.39 → 1.0.41
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/dist/Radio-CfvYyo2v.js +4 -0
- package/dist/{index-A5K9EQi-.js → index-BIHTJGCK.js} +10185 -9731
- package/dist/index.es.js +50 -49
- package/dist/index.umd.js +51 -44
- package/dist/pgo-ui.css +1 -1
- package/package.json +1 -1
- package/src/components/examples/CardViewExample.vue +143 -0
- package/src/components/examples/index.ts +4 -2
- package/src/components/pgo/Card.vue +4 -4
- package/src/components/pgo/CardView.vue +358 -0
- package/src/components/pgo/Modal.vue +1 -1
- package/src/components/pgo/index.ts +2 -1
- package/src/components/pgo/inputs/Select.vue +3 -3
- package/src/pgo-components/__index.js +2 -0
- package/src/pgo-components/pages/Examples.vue +33 -0
- package/src/pgo-components/pages/ListView.vue +89 -36
- package/src/pgo-components/sample-data/card-sample-data.json +524 -0
- package/src/pgo-components/services/axios.js +2 -1
- package/dist/Radio-BVlsk-n_.js +0 -4
package/package.json
CHANGED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="p-6 max-w-7xl mx-auto space-y-6">
|
|
3
|
+
<h2 class="text-2xl font-bold">CardView Example</h2>
|
|
4
|
+
<p class="text-sm text-gray-500">Stack-based card layout with sample expert data ({{ sampleData.length }} cards)</p>
|
|
5
|
+
|
|
6
|
+
<CardView
|
|
7
|
+
:items="sampleData"
|
|
8
|
+
:settings="cardSettings"
|
|
9
|
+
:server-side-options="options"
|
|
10
|
+
:loading="loading"
|
|
11
|
+
lang="dv"
|
|
12
|
+
@update:options="handleOptionsUpdate"
|
|
13
|
+
@action-click="handleActionClick"
|
|
14
|
+
/>
|
|
15
|
+
</div>
|
|
16
|
+
</template>
|
|
17
|
+
|
|
18
|
+
<script setup>
|
|
19
|
+
import { ref } from 'vue'
|
|
20
|
+
import CardView from '../pgo/CardView.vue'
|
|
21
|
+
import sampleJson from '../../pgo-components/sample-data/card-sample-data.json'
|
|
22
|
+
|
|
23
|
+
const loading = ref(false)
|
|
24
|
+
|
|
25
|
+
const sampleData = ref(sampleJson.data)
|
|
26
|
+
|
|
27
|
+
const options = ref({
|
|
28
|
+
page: sampleJson.current_page,
|
|
29
|
+
itemsPerPage: sampleJson.per_page,
|
|
30
|
+
itemsLength: sampleJson.total,
|
|
31
|
+
sortBy: [],
|
|
32
|
+
sortDesc: []
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
const cardSettings = ref({
|
|
36
|
+
grid: { sm: 1, md: 2, lg: 3 },
|
|
37
|
+
scrollHeight: '300px',
|
|
38
|
+
cardLayout: {
|
|
39
|
+
stacks: [
|
|
40
|
+
{
|
|
41
|
+
stackType: 'field',
|
|
42
|
+
key: 'person.nid_pp',
|
|
43
|
+
align: 'right',
|
|
44
|
+
lang: 'en',
|
|
45
|
+
tag: 'h4',
|
|
46
|
+
class: 'font-semibold'
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
stackType: 'row',
|
|
50
|
+
justify: 'space-between',
|
|
51
|
+
align: 'center',
|
|
52
|
+
fields: [
|
|
53
|
+
{ key: 'person.person_name_div', tag: 'h3', class: 'font-bold' },
|
|
54
|
+
{
|
|
55
|
+
stackType: 'button',
|
|
56
|
+
label: { en: 'CV', dv: 'ސީ.ވީ' },
|
|
57
|
+
color: 'info',
|
|
58
|
+
size: 'xs',
|
|
59
|
+
linkKey: 'cv.attachment.url'
|
|
60
|
+
}
|
|
61
|
+
]
|
|
62
|
+
},
|
|
63
|
+
{ stackType: 'divider' },
|
|
64
|
+
{
|
|
65
|
+
stackType: 'chips',
|
|
66
|
+
key: 'categories',
|
|
67
|
+
labelKey: { en: 'label_eng', dv: 'label_div' },
|
|
68
|
+
variant: 'tonal',
|
|
69
|
+
size: 'sm'
|
|
70
|
+
},
|
|
71
|
+
{ stackType: 'divider' },
|
|
72
|
+
{
|
|
73
|
+
stackType: 'section',
|
|
74
|
+
title: { en: 'Education & Training', dv: 'ތަޢުލީމާއި ތަމްރީނު' },
|
|
75
|
+
bgColor: 'bg-teal-50',
|
|
76
|
+
key: 'expertises',
|
|
77
|
+
emptyText: { en: 'No records added', dv: 'ރެކޯޑެއް ނެތް' },
|
|
78
|
+
stacks: [
|
|
79
|
+
{
|
|
80
|
+
stackType: 'row',
|
|
81
|
+
separator: ' - ',
|
|
82
|
+
fields: [
|
|
83
|
+
{ key: 'categories.label_div' },
|
|
84
|
+
{ key: 'issued_date', displayType: 'date' }
|
|
85
|
+
]
|
|
86
|
+
}
|
|
87
|
+
]
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
stackType: 'section',
|
|
91
|
+
title: { en: 'Work Experience', dv: 'ވަޒީފާގެ ތަޖުރިބާ' },
|
|
92
|
+
bgColor: 'bg-green-50',
|
|
93
|
+
key: 'experiences',
|
|
94
|
+
emptyText: { en: 'No records added', dv: 'ރެކޯޑެއް ނެތް' },
|
|
95
|
+
stacks: [
|
|
96
|
+
{
|
|
97
|
+
stackType: 'row',
|
|
98
|
+
separator: ' - ',
|
|
99
|
+
fields: [
|
|
100
|
+
{ key: 'organization' },
|
|
101
|
+
{ key: 'designation' }
|
|
102
|
+
]
|
|
103
|
+
}
|
|
104
|
+
]
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
stackType: 'section',
|
|
108
|
+
title: { en: 'Professional Affiliations', dv: 'ޕްރޮފެޝަނަލް އެފިލިއޭޝަންސް' },
|
|
109
|
+
bgColor: 'bg-lime-50',
|
|
110
|
+
key: 'professional_affiliations',
|
|
111
|
+
emptyText: { en: 'No affiliations added', dv: 'އެފިލިއޭޝަންސް އިތުރު ކޮށްފައި ނެތް' },
|
|
112
|
+
stacks: [
|
|
113
|
+
{ stackType: 'field', key: 'name' }
|
|
114
|
+
]
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
stackType: 'section',
|
|
118
|
+
title: { en: 'Other Experiences', dv: 'އިތުރު ދާއިރާ ތަކުގެ ތަޖުރިބާ' },
|
|
119
|
+
bgColor: 'bg-amber-50',
|
|
120
|
+
key: 'other_experiences',
|
|
121
|
+
emptyText: { en: 'No records added', dv: 'އިތުރު ކޮށްފައި ނެތް' },
|
|
122
|
+
stacks: [
|
|
123
|
+
{ stackType: 'field', key: 'description' }
|
|
124
|
+
]
|
|
125
|
+
}
|
|
126
|
+
]
|
|
127
|
+
}
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
const handleOptionsUpdate = (newOptions) => {
|
|
131
|
+
console.log('Pagination changed:', newOptions)
|
|
132
|
+
// In a real setup, this would fetch new data from the API
|
|
133
|
+
loading.value = true
|
|
134
|
+
setTimeout(() => {
|
|
135
|
+
options.value = { ...options.value, ...newOptions }
|
|
136
|
+
loading.value = false
|
|
137
|
+
}, 500)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const handleActionClick = ({ item, field }) => {
|
|
141
|
+
console.log('Action clicked:', field, 'on item:', item)
|
|
142
|
+
}
|
|
143
|
+
</script>
|
|
@@ -27,6 +27,7 @@ import ThemeToggle from './ThemeToggle.vue'
|
|
|
27
27
|
import TimelineExample from './TimelineExample.vue'
|
|
28
28
|
import TooltipExample from './TooltipExample.vue'
|
|
29
29
|
import VueDatePickerShowcase from './VueDatePickerShowcase.vue'
|
|
30
|
+
import CardViewExample from './CardViewExample.vue'
|
|
30
31
|
|
|
31
32
|
export {
|
|
32
33
|
AppBarExample,
|
|
@@ -55,6 +56,7 @@ export {
|
|
|
55
56
|
ThemeToggle,
|
|
56
57
|
TimelineExample,
|
|
57
58
|
TooltipExample,
|
|
58
|
-
VueDatePickerShowcase
|
|
59
|
+
VueDatePickerShowcase,
|
|
60
|
+
CardViewExample
|
|
59
61
|
|
|
60
|
-
}
|
|
62
|
+
}
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
:class="[headerClass, headerBg, headerPadding, topRoundedClass, headerBd , headerText, `overflow-hidden flex items-center gap-2`]"
|
|
14
14
|
>
|
|
15
15
|
<h3 :class="[titleClass, headerTextSize ]">
|
|
16
|
-
{{ selectedlabels.title ? selectedlabels.title : props.title }}
|
|
16
|
+
{{ selectedlabels.title ? selectedlabels.title : useLanguageSelected(props.title, selectLanguage) }}
|
|
17
17
|
</h3>
|
|
18
18
|
</div>
|
|
19
19
|
</slot>
|
|
@@ -123,8 +123,8 @@ const { language } = inject('i18n')
|
|
|
123
123
|
else {
|
|
124
124
|
Selected = language.value
|
|
125
125
|
}
|
|
126
|
-
console.log('selectLanguage in Card222:', Selected)
|
|
127
|
-
return (Selected === '
|
|
126
|
+
// console.log('selectLanguage in Card222:', Selected)
|
|
127
|
+
return (Selected === 'en') ? 'en' : 'dv'
|
|
128
128
|
})
|
|
129
129
|
|
|
130
130
|
// Provide dir and lang to child components - MOVED HERE AFTER COMPUTED
|
|
@@ -138,7 +138,7 @@ const { language } = inject('i18n')
|
|
|
138
138
|
}));
|
|
139
139
|
|
|
140
140
|
const faruma = computed(() => {
|
|
141
|
-
if (selectLanguage.value
|
|
141
|
+
if (selectLanguage.value != 'en') return 'faruma'
|
|
142
142
|
return ''
|
|
143
143
|
})
|
|
144
144
|
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<!-- Loading bar -->
|
|
4
|
+
<div v-if="loading" class="w-full h-1 bg-primary/20 overflow-hidden rounded">
|
|
5
|
+
<div class="h-full bg-primary animate-[indeterminate_1.5s_infinite_ease-in-out] w-1/3 rounded"></div>
|
|
6
|
+
</div>
|
|
7
|
+
|
|
8
|
+
<!-- Card Grid -->
|
|
9
|
+
<div
|
|
10
|
+
v-if="items && items.length > 0"
|
|
11
|
+
:class="gridClasses"
|
|
12
|
+
class="gap-4 p-2"
|
|
13
|
+
>
|
|
14
|
+
<div
|
|
15
|
+
v-for="(item, index) in items"
|
|
16
|
+
:key="item[itemKey] ?? index"
|
|
17
|
+
class="min-h-full"
|
|
18
|
+
>
|
|
19
|
+
<Card
|
|
20
|
+
bg="bg-background-color"
|
|
21
|
+
border="border border-input-border"
|
|
22
|
+
rounded="lg"
|
|
23
|
+
shadow="shadow-md"
|
|
24
|
+
padding="p-3"
|
|
25
|
+
margin=""
|
|
26
|
+
:lang="lang"
|
|
27
|
+
class="h-full"
|
|
28
|
+
>
|
|
29
|
+
<div class="flex flex-col gap-1">
|
|
30
|
+
<template v-for="(stack, sIdx) in stacks" :key="sIdx">
|
|
31
|
+
<!-- Field -->
|
|
32
|
+
<div
|
|
33
|
+
v-if="stack.stackType === 'field'"
|
|
34
|
+
:class="[fieldAlignClass(stack), stack.class || '']"
|
|
35
|
+
:dir="stack.lang === 'en' ? 'ltr' : stack.lang === 'dv' ? 'rtl' : ''"
|
|
36
|
+
>
|
|
37
|
+
<component :is="stack.tag || 'span'">
|
|
38
|
+
{{ getNestedValue(item, stack.key) ?? '' }}
|
|
39
|
+
</component>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<!-- Row -->
|
|
43
|
+
<div
|
|
44
|
+
v-else-if="stack.stackType === 'row'"
|
|
45
|
+
class="flex items-center gap-2"
|
|
46
|
+
:class="rowJustifyClass(stack)"
|
|
47
|
+
>
|
|
48
|
+
<template v-for="(field, fIdx) in stack.fields" :key="fIdx">
|
|
49
|
+
<!-- Separator text between fields (not before the first) -->
|
|
50
|
+
<span
|
|
51
|
+
v-if="Number(fIdx) > 0 && stack.separator"
|
|
52
|
+
class="text-gray-400 text-sm"
|
|
53
|
+
>{{ stack.separator }}</span>
|
|
54
|
+
|
|
55
|
+
<!-- Button type inside row -->
|
|
56
|
+
<Button
|
|
57
|
+
v-if="field.stackType === 'button'"
|
|
58
|
+
:size="field.size || 'sm'"
|
|
59
|
+
:color="field.color || 'primary'"
|
|
60
|
+
@click="handleActionClick(item, field)"
|
|
61
|
+
>
|
|
62
|
+
{{ resolveLocalizedValue(field.label) }}
|
|
63
|
+
</Button>
|
|
64
|
+
|
|
65
|
+
<!-- Regular field inside row -->
|
|
66
|
+
<component
|
|
67
|
+
v-else
|
|
68
|
+
:is="field.tag || 'span'"
|
|
69
|
+
:class="[field.class || '']"
|
|
70
|
+
:dir="field.lang === 'en' ? 'ltr' : field.lang === 'dv' ? 'rtl' : ''"
|
|
71
|
+
>
|
|
72
|
+
{{ formatFieldValue(getNestedValue(item, field.key), field) }}
|
|
73
|
+
</component>
|
|
74
|
+
</template>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<!-- Divider -->
|
|
78
|
+
<hr
|
|
79
|
+
v-else-if="stack.stackType === 'divider'"
|
|
80
|
+
class="border-input-border my-1"
|
|
81
|
+
/>
|
|
82
|
+
|
|
83
|
+
<!-- Chips -->
|
|
84
|
+
<div
|
|
85
|
+
v-else-if="stack.stackType === 'chips'"
|
|
86
|
+
class="flex flex-wrap gap-1 py-1"
|
|
87
|
+
>
|
|
88
|
+
<Chip
|
|
89
|
+
v-for="(chipItem, cIdx) in (getNestedValue(item, stack.key) || [])"
|
|
90
|
+
:key="cIdx"
|
|
91
|
+
:label="resolveChipLabel(chipItem, stack.labelKey)"
|
|
92
|
+
:variant="stack.variant || 'tonal'"
|
|
93
|
+
:size="stack.size || 'sm'"
|
|
94
|
+
:color="stack.color || 'primary'"
|
|
95
|
+
/>
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
<!-- Section (iterates over array field, renders nested stacks per item) -->
|
|
99
|
+
<div
|
|
100
|
+
v-else-if="stack.stackType === 'section'"
|
|
101
|
+
:class="[stack.bgColor || '', 'rounded-lg p-2 my-1']"
|
|
102
|
+
>
|
|
103
|
+
<h3 class="font-semibold text-sm mb-1">
|
|
104
|
+
{{ resolveLocalizedValue(stack.title) }}
|
|
105
|
+
</h3>
|
|
106
|
+
|
|
107
|
+
<!-- Section items -->
|
|
108
|
+
<div
|
|
109
|
+
v-if="getNestedValue(item, stack.key) && getNestedValue(item, stack.key).length > 0"
|
|
110
|
+
>
|
|
111
|
+
<div
|
|
112
|
+
v-for="(subItem, subIdx) in getNestedValue(item, stack.key)"
|
|
113
|
+
:key="subIdx"
|
|
114
|
+
class="py-0.5"
|
|
115
|
+
>
|
|
116
|
+
<!-- Render nested stacks for each sub-item -->
|
|
117
|
+
<template v-for="(subStack, ssIdx) in (stack.stacks || [])" :key="ssIdx">
|
|
118
|
+
<!-- Nested field -->
|
|
119
|
+
<div
|
|
120
|
+
v-if="subStack.stackType === 'field'"
|
|
121
|
+
:class="[fieldAlignClass(subStack), subStack.class || '', 'text-sm']"
|
|
122
|
+
>
|
|
123
|
+
<component :is="subStack.tag || 'span'">
|
|
124
|
+
{{ formatFieldValue(getNestedValue(subItem, subStack.key), subStack) }}
|
|
125
|
+
</component>
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
<!-- Nested row -->
|
|
129
|
+
<div
|
|
130
|
+
v-else-if="subStack.stackType === 'row'"
|
|
131
|
+
class="flex items-center gap-1 text-sm"
|
|
132
|
+
:class="rowJustifyClass(subStack)"
|
|
133
|
+
>
|
|
134
|
+
<template v-for="(subField, sfIdx) in subStack.fields" :key="sfIdx">
|
|
135
|
+
<span
|
|
136
|
+
v-if="Number(sfIdx) > 0 && subStack.separator"
|
|
137
|
+
class="text-gray-400"
|
|
138
|
+
>{{ subStack.separator }}</span>
|
|
139
|
+
<component
|
|
140
|
+
:is="subField.tag || 'span'"
|
|
141
|
+
:class="[subField.class || '']"
|
|
142
|
+
>
|
|
143
|
+
{{ formatFieldValue(getNestedValue(subItem, subField.key), subField) }}
|
|
144
|
+
</component>
|
|
145
|
+
</template>
|
|
146
|
+
</div>
|
|
147
|
+
</template>
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
<!-- Empty state -->
|
|
152
|
+
<p
|
|
153
|
+
v-else
|
|
154
|
+
class="text-gray-400 text-center text-sm py-2"
|
|
155
|
+
>
|
|
156
|
+
{{ resolveLocalizedValue(stack.emptyText) }}
|
|
157
|
+
</p>
|
|
158
|
+
</div>
|
|
159
|
+
</template>
|
|
160
|
+
</div>
|
|
161
|
+
</Card>
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
|
|
165
|
+
<!-- Empty state -->
|
|
166
|
+
<div v-else-if="!loading" class="text-center py-8 text-gray-400">
|
|
167
|
+
{{ resolveLocalizedValue(noDataText) || 'No data available' }}
|
|
168
|
+
</div>
|
|
169
|
+
|
|
170
|
+
<!-- Scrollable area wrapper (applied via scrollHeight setting) -->
|
|
171
|
+
|
|
172
|
+
<!-- Pagination -->
|
|
173
|
+
<Pagination
|
|
174
|
+
v-if="!hidePagination && serverSideOptions?.itemsLength > 0"
|
|
175
|
+
:page="serverSideOptions.page"
|
|
176
|
+
:items-per-page="serverSideOptions.itemsPerPage"
|
|
177
|
+
:items-length="serverSideOptions.itemsLength"
|
|
178
|
+
:items-per-page-options="itemsPerPageOptions"
|
|
179
|
+
@change="handlePaginationChange"
|
|
180
|
+
/>
|
|
181
|
+
</div>
|
|
182
|
+
</template>
|
|
183
|
+
|
|
184
|
+
<script setup>
|
|
185
|
+
import { computed, inject } from 'vue'
|
|
186
|
+
import Card from './Card.vue'
|
|
187
|
+
import Chip from './buttons/Chip.vue'
|
|
188
|
+
import Button from './Button.vue'
|
|
189
|
+
import Pagination from './Pagination.vue'
|
|
190
|
+
|
|
191
|
+
const { language } = inject('i18n')
|
|
192
|
+
|
|
193
|
+
const props = defineProps({
|
|
194
|
+
items: {
|
|
195
|
+
type: Array,
|
|
196
|
+
default: () => []
|
|
197
|
+
},
|
|
198
|
+
settings: {
|
|
199
|
+
type: Object,
|
|
200
|
+
default: () => ({})
|
|
201
|
+
},
|
|
202
|
+
serverSideOptions: {
|
|
203
|
+
type: Object,
|
|
204
|
+
default: () => ({})
|
|
205
|
+
},
|
|
206
|
+
loading: {
|
|
207
|
+
type: Boolean,
|
|
208
|
+
default: false
|
|
209
|
+
},
|
|
210
|
+
lang: {
|
|
211
|
+
type: String,
|
|
212
|
+
default: 'dv'
|
|
213
|
+
},
|
|
214
|
+
noDataText: {
|
|
215
|
+
type: [String, Object],
|
|
216
|
+
default: ''
|
|
217
|
+
},
|
|
218
|
+
hidePagination: {
|
|
219
|
+
type: Boolean,
|
|
220
|
+
default: false
|
|
221
|
+
},
|
|
222
|
+
itemsPerPageOptions: {
|
|
223
|
+
type: Array,
|
|
224
|
+
default: () => [9, 18, 27, 36]
|
|
225
|
+
},
|
|
226
|
+
itemKey: {
|
|
227
|
+
type: String,
|
|
228
|
+
default: 'id'
|
|
229
|
+
}
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
const emit = defineEmits(['update:options', 'card-click', 'action-click'])
|
|
233
|
+
|
|
234
|
+
// --- Computed ---
|
|
235
|
+
const stacks = computed(() => props.settings?.cardLayout?.stacks || [])
|
|
236
|
+
|
|
237
|
+
const gridConfig = computed(() => props.settings?.grid || { sm: 1, md: 2, lg: 3 })
|
|
238
|
+
|
|
239
|
+
const gridClasses = computed(() => {
|
|
240
|
+
const sm = gridConfig.value.sm || 1
|
|
241
|
+
const md = gridConfig.value.md || 2
|
|
242
|
+
const lg = gridConfig.value.lg || 3
|
|
243
|
+
|
|
244
|
+
const colMap = {
|
|
245
|
+
1: 'grid-cols-1',
|
|
246
|
+
2: 'grid-cols-2',
|
|
247
|
+
3: 'grid-cols-3',
|
|
248
|
+
4: 'grid-cols-4',
|
|
249
|
+
5: 'grid-cols-5',
|
|
250
|
+
6: 'grid-cols-6'
|
|
251
|
+
}
|
|
252
|
+
const mdMap = {
|
|
253
|
+
1: 'md:grid-cols-1',
|
|
254
|
+
2: 'md:grid-cols-2',
|
|
255
|
+
3: 'md:grid-cols-3',
|
|
256
|
+
4: 'md:grid-cols-4',
|
|
257
|
+
5: 'md:grid-cols-5',
|
|
258
|
+
6: 'md:grid-cols-6'
|
|
259
|
+
}
|
|
260
|
+
const lgMap = {
|
|
261
|
+
1: 'lg:grid-cols-1',
|
|
262
|
+
2: 'lg:grid-cols-2',
|
|
263
|
+
3: 'lg:grid-cols-3',
|
|
264
|
+
4: 'lg:grid-cols-4',
|
|
265
|
+
5: 'lg:grid-cols-5',
|
|
266
|
+
6: 'lg:grid-cols-6'
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return `grid ${colMap[sm] || 'grid-cols-1'} ${mdMap[md] || 'md:grid-cols-2'} ${lgMap[lg] || 'lg:grid-cols-3'}`
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
// --- Helpers ---
|
|
273
|
+
const getNestedValue = (obj, path) => {
|
|
274
|
+
if (!path || !obj) return undefined
|
|
275
|
+
return path.split('.').reduce((current, key) => current?.[key], obj)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const resolveLocalizedValue = (val) => {
|
|
279
|
+
if (!val) return ''
|
|
280
|
+
if (typeof val === 'string') return val
|
|
281
|
+
if (typeof val === 'object') {
|
|
282
|
+
const lang = props.lang || language?.value || 'dv'
|
|
283
|
+
return val[lang] ?? val['en'] ?? ''
|
|
284
|
+
}
|
|
285
|
+
return ''
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const resolveChipLabel = (chipItem, labelKey) => {
|
|
289
|
+
if (!labelKey) return String(chipItem ?? '')
|
|
290
|
+
if (typeof labelKey === 'object') {
|
|
291
|
+
const lang = props.lang || language?.value || 'dv'
|
|
292
|
+
const key = labelKey[lang] || labelKey['en']
|
|
293
|
+
return key ? (getNestedValue(chipItem, key) ?? '') : String(chipItem ?? '')
|
|
294
|
+
}
|
|
295
|
+
return getNestedValue(chipItem, labelKey) ?? ''
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const formatFieldValue = (value, field) => {
|
|
299
|
+
if (value == null) return ''
|
|
300
|
+
if (field?.displayType === 'date' && value) {
|
|
301
|
+
try {
|
|
302
|
+
return new Date(value).toLocaleDateString()
|
|
303
|
+
} catch {
|
|
304
|
+
return value
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return value
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const fieldAlignClass = (stack) => {
|
|
311
|
+
const alignMap = {
|
|
312
|
+
'left': 'text-left',
|
|
313
|
+
'right': 'text-right',
|
|
314
|
+
'center': 'text-center'
|
|
315
|
+
}
|
|
316
|
+
return alignMap[stack.align] || ''
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const rowJustifyClass = (stack) => {
|
|
320
|
+
const justifyMap = {
|
|
321
|
+
'space-between': 'justify-between',
|
|
322
|
+
'center': 'justify-center',
|
|
323
|
+
'start': 'justify-start',
|
|
324
|
+
'end': 'justify-end',
|
|
325
|
+
'around': 'justify-around',
|
|
326
|
+
'evenly': 'justify-evenly'
|
|
327
|
+
}
|
|
328
|
+
return justifyMap[stack.justify] || ''
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// --- Events ---
|
|
332
|
+
const handlePaginationChange = ({ page, itemsPerPage }) => {
|
|
333
|
+
emit('update:options', {
|
|
334
|
+
page,
|
|
335
|
+
itemsPerPage,
|
|
336
|
+
sortBy: props.serverSideOptions?.sortBy || [],
|
|
337
|
+
sortDesc: props.serverSideOptions?.sortDesc || []
|
|
338
|
+
})
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const handleActionClick = (item, field) => {
|
|
342
|
+
if (field.linkKey) {
|
|
343
|
+
const url = getNestedValue(item, field.linkKey)
|
|
344
|
+
if (url) {
|
|
345
|
+
window.open(url, '_blank', 'noopener,noreferrer')
|
|
346
|
+
return
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
emit('action-click', { item, field })
|
|
350
|
+
}
|
|
351
|
+
</script>
|
|
352
|
+
|
|
353
|
+
<style scoped>
|
|
354
|
+
@keyframes indeterminate {
|
|
355
|
+
0% { transform: translateX(-100%); }
|
|
356
|
+
100% { transform: translateX(400%); }
|
|
357
|
+
}
|
|
358
|
+
</style>
|
|
@@ -92,7 +92,7 @@
|
|
|
92
92
|
}) },
|
|
93
93
|
width: { type: String, default: '' },
|
|
94
94
|
height: { type: String, default: '' },
|
|
95
|
-
title: { type: [String, Object], default: '' },
|
|
95
|
+
title: { type: [String, Object, Array], default: '' },
|
|
96
96
|
cardClass: { type: String, default: '' },
|
|
97
97
|
overflow: { type: String, default: 'max-h-[calc(100vh-150px)] overflow-auto' },
|
|
98
98
|
bg: { type: String, default: 'bg-background-color' },
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import Button from './Button.vue'
|
|
2
2
|
import Card from './Card.vue'
|
|
3
|
+
import CardView from './CardView.vue'
|
|
3
4
|
import HeroIcon from './HeroIcon.vue'
|
|
4
5
|
|
|
5
6
|
import AppBar from './AppBar.vue'
|
|
@@ -50,7 +51,7 @@ import SearchBox from './SearchBox.vue'
|
|
|
50
51
|
|
|
51
52
|
|
|
52
53
|
// export { Chip, ChipGroup, DataTable, Dropdown, FileUpload, Form }
|
|
53
|
-
export { Button, Card, HeroIcon,
|
|
54
|
+
export { Button, Card, CardView, HeroIcon,
|
|
54
55
|
AppBar,
|
|
55
56
|
Avatar,
|
|
56
57
|
Banner,
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
:required="attrs.required"
|
|
42
42
|
:dir="attrs.dir"
|
|
43
43
|
type="text"
|
|
44
|
-
:placeholder="isFocused &&
|
|
44
|
+
:placeholder="!isFocused || (!isFocused && searchQuery) || selectedLabel ? '' : useLanguageSelected(placeholder)"
|
|
45
45
|
:class="inputClasses"
|
|
46
46
|
@focus="handleFocus"
|
|
47
47
|
@blur="handleBlur"
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
/>
|
|
50
50
|
|
|
51
51
|
<!-- Single select label -->
|
|
52
|
-
<div v-if="selectedLabel && !multiple" :class="['absolute', computedLang
|
|
52
|
+
<div v-if="selectedLabel && !multiple" :class="['absolute', computedLang == 'dv' ? 'right-0' : 'left-0', ' px-3 truncate pointer-events-none']">
|
|
53
53
|
{{ selectedLabel }}
|
|
54
54
|
</div>
|
|
55
55
|
<!-- Multi-select badges - Show BEFORE input -->
|
|
@@ -254,7 +254,7 @@
|
|
|
254
254
|
const baseUrl = inject('baseUrl')
|
|
255
255
|
|
|
256
256
|
const computedDir = computed(() => props.dir || cardDir)
|
|
257
|
-
const computedLang = computed(() => props.lang ||
|
|
257
|
+
const computedLang = computed(() => props.lang || language.value)
|
|
258
258
|
|
|
259
259
|
const isOpen = ref(false)
|
|
260
260
|
|
|
@@ -16,6 +16,7 @@ import Footer from '../components/pgo/Footer.vue'
|
|
|
16
16
|
import PApp from '../components/pgo/PApp.vue'
|
|
17
17
|
import Button from '../components/pgo/Button.vue'
|
|
18
18
|
import Card from '../components/pgo/Card.vue'
|
|
19
|
+
import CardView from '../components/pgo/CardView.vue'
|
|
19
20
|
import Select from '../components/pgo/inputs/Select.vue'
|
|
20
21
|
import AppBar from '../components/pgo/AppBar.vue'
|
|
21
22
|
import NavigationDrawer from '../components/pgo/NavigationDrawer.vue'
|
|
@@ -45,6 +46,7 @@ export {
|
|
|
45
46
|
PApp,
|
|
46
47
|
Button,
|
|
47
48
|
Card,
|
|
49
|
+
CardView,
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
// Export plugin
|
|
@@ -239,6 +239,7 @@ import {
|
|
|
239
239
|
TimelineExample,
|
|
240
240
|
TooltipExample,
|
|
241
241
|
VueDatePickerShowcase,
|
|
242
|
+
CardViewExample,
|
|
242
243
|
} from '@/components/examples/index.ts'
|
|
243
244
|
|
|
244
245
|
// ─── Example component map (index → component) ────────────────────────────
|
|
@@ -269,6 +270,7 @@ const exampleMap = [
|
|
|
269
270
|
TimelineExample, // 23
|
|
270
271
|
TooltipExample, // 24
|
|
271
272
|
VueDatePickerShowcase, // 25
|
|
273
|
+
CardViewExample, // 26
|
|
272
274
|
]
|
|
273
275
|
|
|
274
276
|
// ─── Category definitions with colour tokens ──────────────────────────────
|
|
@@ -356,6 +358,7 @@ const categories = [
|
|
|
356
358
|
badgeClass: 'text-cyan-600 border-cyan-300 bg-cyan-50',
|
|
357
359
|
items: [
|
|
358
360
|
{ name: 'DataTable', index: 19 },
|
|
361
|
+
{ name: 'CardView', index: 26 },
|
|
359
362
|
],
|
|
360
363
|
},
|
|
361
364
|
{
|
|
@@ -1390,6 +1393,36 @@ const docs = [
|
|
|
1390
1393
|
slots: [],
|
|
1391
1394
|
},
|
|
1392
1395
|
|
|
1396
|
+
// ── 26 · CardView ───────────────────────
|
|
1397
|
+
{
|
|
1398
|
+
name: 'CardView',
|
|
1399
|
+
description: 'A stack-based card layout for displaying data in a responsive grid of cards. Supports configurable stacks: field, row (with separator), divider, chips, and recursive section. Driven by a cardLayout config from CCS.',
|
|
1400
|
+
usage:
|
|
1401
|
+
`<CardView
|
|
1402
|
+
:items="items"
|
|
1403
|
+
:settings="{ grid: { sm: 1, md: 2, lg: 3 }, scrollHeight: '300px', cardLayout: { stacks: [...] } }"
|
|
1404
|
+
:server-side-options="options"
|
|
1405
|
+
lang="en"
|
|
1406
|
+
@update:options="handlePagination"
|
|
1407
|
+
@action-click="handleAction"
|
|
1408
|
+
/>`,
|
|
1409
|
+
props: [
|
|
1410
|
+
{ name: 'items', type: 'Array', default: '[]', description: 'Array of data objects to render as cards.' },
|
|
1411
|
+
{ name: 'settings', type: 'Object', default: '{}', description: 'Card settings: grid, scrollHeight, cardLayout (stacks array).' },
|
|
1412
|
+
{ name: 'serverSideOptions', type: 'Object', default: '{}', description: 'Pagination state: page, itemsPerPage, itemsLength, sortBy, sortDesc.' },
|
|
1413
|
+
{ name: 'loading', type: 'Boolean', default: 'false', description: 'Show loading overlay.' },
|
|
1414
|
+
{ name: 'lang', type: 'String', default: "'en'", description: 'Language code for localized label resolution.' },
|
|
1415
|
+
{ name: 'noDataText', type: 'String|Object', default: "'No data'", description: 'Text shown when items is empty.' },
|
|
1416
|
+
{ name: 'hidePagination', type: 'Boolean', default: 'false', description: 'Hide the pagination footer.' },
|
|
1417
|
+
],
|
|
1418
|
+
events: [
|
|
1419
|
+
{ name: 'update:options', payload: 'Object', description: 'Emitted when pagination changes (page, itemsPerPage).' },
|
|
1420
|
+
{ name: 'card-click', payload: '{ item }', description: 'Emitted when a card is clicked.' },
|
|
1421
|
+
{ name: 'action-click', payload: '{ item, field }', description: 'Emitted when a button stack is clicked.' },
|
|
1422
|
+
],
|
|
1423
|
+
slots: [],
|
|
1424
|
+
},
|
|
1425
|
+
|
|
1393
1426
|
]
|
|
1394
1427
|
</script>
|
|
1395
1428
|
|