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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pgo-ui",
3
- "version": "1.0.39",
3
+ "version": "1.0.41",
4
4
  "description": "A Vue 3 component library with PGO design system",
5
5
  "private": false,
6
6
  "type": "module",
@@ -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 === 'dv') ? 'dv' : 'en'
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 == 'dv') return 'faruma'
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 && !searchQuery ? useLanguageSelected(placeholder) : ''"
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 || language === 'dv' ? 'right-0' : 'left-0', ' px-3 truncate pointer-events-none']">
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 || cardLang)
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