@5minds/node-red-dashboard-2-processcube-dynamic-table 2.0.3 → 2.0.4-develop-d30ad5-mfzfpl05
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/nodes/dynamic-table.html +392 -369
- package/nodes/dynamic-table.js +11 -1
- package/package.json +89 -88
- package/resources/ui-dynamic-table.umd.js +22 -19
- package/ui/components/UIDynamicTable.vue +429 -50
|
@@ -1,31 +1,64 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="ui-dynamic-table-container">
|
|
3
|
-
<UIDynamicTableTitleText
|
|
4
|
-
|
|
5
|
-
:
|
|
6
|
-
|
|
7
|
-
:
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
3
|
+
<UIDynamicTableTitleText
|
|
4
|
+
v-if="props.title_text && props.title_text.length > 0"
|
|
5
|
+
:title="props.title_text"
|
|
6
|
+
:style="props.title_style || 'default'"
|
|
7
|
+
:customStyles="props.title_custom_text_styling || ''"
|
|
8
|
+
:titleIcon="props.title_icon || ''"
|
|
9
|
+
/>
|
|
10
|
+
|
|
11
|
+
<!-- Search and Reload Controls -->
|
|
12
|
+
<v-toolbar flat class="pb-4 pt-8">
|
|
13
|
+
<v-text-field
|
|
14
|
+
class="search-input"
|
|
15
|
+
:class="{ 'mobile-search': isMobile }"
|
|
16
|
+
v-model="search"
|
|
17
|
+
label="Suchen"
|
|
18
|
+
prepend-inner-icon="mdi-magnify"
|
|
19
|
+
variant="outlined"
|
|
20
|
+
hide-details
|
|
21
|
+
single-line
|
|
22
|
+
>
|
|
23
|
+
</v-text-field>
|
|
24
|
+
<v-btn
|
|
25
|
+
class="reload-button ml-4"
|
|
26
|
+
@click="reloadData"
|
|
27
|
+
:disabled="isReloading"
|
|
28
|
+
size="small"
|
|
29
|
+
variant="outlined"
|
|
30
|
+
>
|
|
31
|
+
<v-icon left :class="{ 'reload-spin': isReloading }">mdi-refresh</v-icon>
|
|
32
|
+
</v-btn>
|
|
33
|
+
</v-toolbar>
|
|
34
|
+
|
|
35
|
+
<!-- Desktop Table View -->
|
|
36
|
+
<v-data-table
|
|
37
|
+
v-show="!isMobile"
|
|
38
|
+
:headers="headers"
|
|
39
|
+
:items="filteredTasks"
|
|
40
|
+
:sort-by="sortBy"
|
|
41
|
+
:items-per-page="itemsPerPage"
|
|
42
|
+
:items-per-page-options="itemsPerPageOptions"
|
|
43
|
+
@update:sort-by="updateSortBy"
|
|
44
|
+
@update:items-per-page="updateItemsPerPage"
|
|
45
|
+
class="full-width-table"
|
|
46
|
+
>
|
|
21
47
|
<template v-slot:item.actions="{ item }">
|
|
22
48
|
<v-container class="action-button-container">
|
|
23
|
-
<v-container
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
49
|
+
<v-container
|
|
50
|
+
v-for="(action, index) in actions"
|
|
51
|
+
style="padding: 0px; display: inline; width: fit-content; margin: 0px"
|
|
52
|
+
:key="index"
|
|
53
|
+
>
|
|
54
|
+
<v-btn
|
|
55
|
+
style="margin: 0px 2px"
|
|
56
|
+
class="action-button"
|
|
57
|
+
v-if="action && conditionCheck(action.condition, item)"
|
|
58
|
+
size="small"
|
|
59
|
+
@click="actionFn(action, item)"
|
|
60
|
+
>
|
|
61
|
+
{{ getActionLabel(action, item) }}
|
|
29
62
|
</v-btn>
|
|
30
63
|
</v-container>
|
|
31
64
|
</v-container>
|
|
@@ -34,18 +67,63 @@
|
|
|
34
67
|
<v-alert text="Keine Daten" style="margin: 12px"></v-alert>
|
|
35
68
|
</template>
|
|
36
69
|
</v-data-table>
|
|
70
|
+
|
|
71
|
+
<!-- Mobile Cards View -->
|
|
72
|
+
<div v-show="isMobile" class="mobile-cards-container">
|
|
73
|
+
<v-card
|
|
74
|
+
v-for="(item, index) in paginatedTasksForMobile"
|
|
75
|
+
:key="index"
|
|
76
|
+
class="mobile-card ma-2"
|
|
77
|
+
variant="outlined"
|
|
78
|
+
elevation="2"
|
|
79
|
+
>
|
|
80
|
+
<v-card-text class="pb-2">
|
|
81
|
+
<div v-for="header in dataHeaders" :key="header.key" class="mobile-card-row">
|
|
82
|
+
<span class="mobile-card-label">{{ header.title }}:</span>
|
|
83
|
+
<span class="mobile-card-value">{{ item[header.key] }}</span>
|
|
84
|
+
</div>
|
|
85
|
+
</v-card-text>
|
|
86
|
+
<v-card-actions v-if="actions && actions.length > 0" class="pt-0 px-2 pb-2 d-flex">
|
|
87
|
+
<v-btn
|
|
88
|
+
v-for="(action, actionIndex) in visibleActionsForMobile"
|
|
89
|
+
:key="actionIndex"
|
|
90
|
+
size="x-small"
|
|
91
|
+
color="primary"
|
|
92
|
+
variant="flat"
|
|
93
|
+
class="flex-grow-1 mx-1"
|
|
94
|
+
v-if="action && conditionCheck(action.condition, item)"
|
|
95
|
+
style="min-height: 28px; height: 28px; font-size: 0.75rem"
|
|
96
|
+
@click="actionFn(action, item)"
|
|
97
|
+
>
|
|
98
|
+
{{ getActionLabel(action, item) }}
|
|
99
|
+
</v-btn>
|
|
100
|
+
</v-card-actions>
|
|
101
|
+
</v-card>
|
|
102
|
+
|
|
103
|
+
<!-- No Data Alert for Mobile -->
|
|
104
|
+
<v-alert v-if="paginatedTasksForMobile.length === 0" text="Keine Daten" class="ma-2"></v-alert>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
<!-- Pagination for Mobile -->
|
|
108
|
+
<v-pagination
|
|
109
|
+
v-if="isMobile && filteredTasks.length > itemsPerPage"
|
|
110
|
+
v-model="currentPage"
|
|
111
|
+
:length="Math.ceil(filteredTasks.length / itemsPerPage)"
|
|
112
|
+
class="mt-4"
|
|
113
|
+
></v-pagination>
|
|
37
114
|
</div>
|
|
38
115
|
</template>
|
|
39
116
|
|
|
40
117
|
<script>
|
|
41
118
|
import { mapState } from 'vuex';
|
|
42
|
-
import UIDynamicTableTitleText from './TitleText.vue'
|
|
119
|
+
import UIDynamicTableTitleText from './TitleText.vue';
|
|
43
120
|
import { debounce } from 'lodash';
|
|
121
|
+
import jsonata from 'jsonata';
|
|
44
122
|
|
|
45
123
|
export default {
|
|
46
124
|
name: 'UIDynamicTable',
|
|
47
125
|
components: {
|
|
48
|
-
UIDynamicTableTitleText
|
|
126
|
+
UIDynamicTableTitleText,
|
|
49
127
|
},
|
|
50
128
|
inject: ['$socket'],
|
|
51
129
|
props: {
|
|
@@ -67,6 +145,53 @@ export default {
|
|
|
67
145
|
itemsPerPageStorageKey() {
|
|
68
146
|
return `table_items_per_page_${this.id}`;
|
|
69
147
|
},
|
|
148
|
+
isMobile() {
|
|
149
|
+
return this.$vuetify.display.mobile;
|
|
150
|
+
},
|
|
151
|
+
dataHeaders() {
|
|
152
|
+
return this.headers.filter((header) => header.key !== 'actions');
|
|
153
|
+
},
|
|
154
|
+
filteredTasks() {
|
|
155
|
+
let filtered = this.tasks;
|
|
156
|
+
|
|
157
|
+
// Apply search filter
|
|
158
|
+
if (this.search && this.search.trim() !== '') {
|
|
159
|
+
const searchTerm = this.search.toLowerCase();
|
|
160
|
+
filtered = filtered.filter((item) => {
|
|
161
|
+
return this.dataHeaders.some((header) => {
|
|
162
|
+
const value = item[header.key];
|
|
163
|
+
return value && value.toString().toLowerCase().includes(searchTerm);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Don't apply pagination here - let v-data-table handle it for desktop
|
|
169
|
+
// For mobile, we'll handle pagination separately
|
|
170
|
+
return filtered;
|
|
171
|
+
},
|
|
172
|
+
paginatedTasksForMobile() {
|
|
173
|
+
if (!this.isMobile) return this.filteredTasks;
|
|
174
|
+
|
|
175
|
+
const startIndex = (this.currentPage - 1) * this.itemsPerPage;
|
|
176
|
+
const endIndex = startIndex + this.itemsPerPage;
|
|
177
|
+
return this.filteredTasks.slice(startIndex, endIndex);
|
|
178
|
+
},
|
|
179
|
+
visibleActionsForMobile() {
|
|
180
|
+
return this.actions.filter((action) => {
|
|
181
|
+
if (!action) return false;
|
|
182
|
+
|
|
183
|
+
// If no condition or empty condition, show the action
|
|
184
|
+
if (!action.condition || action.condition === '') return true;
|
|
185
|
+
|
|
186
|
+
// For conditions, we'll show all actions and let the click handler deal with it
|
|
187
|
+
// This avoids the Vue reactivity issue with calling conditionCheck in template
|
|
188
|
+
return true;
|
|
189
|
+
});
|
|
190
|
+
},
|
|
191
|
+
// Computed property to ensure reactivity to message changes
|
|
192
|
+
currentMessage() {
|
|
193
|
+
return this.messages[this.id] || {};
|
|
194
|
+
},
|
|
70
195
|
},
|
|
71
196
|
data() {
|
|
72
197
|
return {
|
|
@@ -78,14 +203,17 @@ export default {
|
|
|
78
203
|
isInitialized: false,
|
|
79
204
|
sortBy: [],
|
|
80
205
|
itemsPerPage: 10,
|
|
206
|
+
currentPage: 1,
|
|
81
207
|
itemsPerPageOptions: [
|
|
82
208
|
{ value: 5, title: '5' },
|
|
83
209
|
{ value: 10, title: '10' },
|
|
84
210
|
{ value: 25, title: '25' },
|
|
85
211
|
{ value: 50, title: '50' },
|
|
86
212
|
{ value: 100, title: '100' },
|
|
87
|
-
{ value: -1, title: 'All' }
|
|
213
|
+
{ value: -1, title: 'All' },
|
|
88
214
|
],
|
|
215
|
+
// Store JSONata evaluation results for each action-row combination
|
|
216
|
+
jsonataResults: new Map(),
|
|
89
217
|
};
|
|
90
218
|
},
|
|
91
219
|
mounted() {
|
|
@@ -125,6 +253,14 @@ export default {
|
|
|
125
253
|
this.$socket.emit('widget-action', this.id, msgArr);
|
|
126
254
|
},
|
|
127
255
|
actionFn(action, item) {
|
|
256
|
+
// Check condition at click time if it exists
|
|
257
|
+
if (action.condition && action.condition !== '') {
|
|
258
|
+
if (!this.conditionCheck(action.condition, item)) {
|
|
259
|
+
console.log('Action condition not met:', action.condition);
|
|
260
|
+
return; // Don't execute if condition fails
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
128
264
|
const msg = this.messages[this.id] || {};
|
|
129
265
|
msg.payload = item;
|
|
130
266
|
this.send(
|
|
@@ -133,10 +269,31 @@ export default {
|
|
|
133
269
|
);
|
|
134
270
|
},
|
|
135
271
|
initialize(msg) {
|
|
136
|
-
|
|
137
|
-
|
|
272
|
+
// Handle null or undefined messages
|
|
273
|
+
if (!msg) {
|
|
274
|
+
this.tasks = [];
|
|
275
|
+
this.actions = (this.props.options || []).filter((action) => action != null);
|
|
276
|
+
this.headers = (this.props.columns || []).map((item) => ({
|
|
277
|
+
title: item.label,
|
|
278
|
+
key: item.value,
|
|
279
|
+
}));
|
|
280
|
+
this.headers.push({
|
|
281
|
+
title: '',
|
|
282
|
+
align: 'center',
|
|
283
|
+
key: 'actions',
|
|
284
|
+
sortable: false,
|
|
285
|
+
});
|
|
286
|
+
// Clear JSONata results cache
|
|
287
|
+
this.jsonataResults.clear();
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
this.tasks = msg.payload || [];
|
|
138
292
|
|
|
139
|
-
|
|
293
|
+
// Always use props.options for frontend-only evaluation
|
|
294
|
+
this.actions = (this.props.options || []).filter((action) => action != null);
|
|
295
|
+
|
|
296
|
+
this.headers = (this.props.columns || []).map((item) => ({
|
|
140
297
|
title: item.label,
|
|
141
298
|
key: item.value,
|
|
142
299
|
}));
|
|
@@ -145,11 +302,156 @@ export default {
|
|
|
145
302
|
title: '',
|
|
146
303
|
align: 'center',
|
|
147
304
|
key: 'actions',
|
|
148
|
-
sortable: false
|
|
305
|
+
sortable: false,
|
|
149
306
|
});
|
|
307
|
+
|
|
308
|
+
// Clear JSONata results cache when new data arrives
|
|
309
|
+
this.jsonataResults.clear();
|
|
310
|
+
},
|
|
311
|
+
getActionLabel(action, item) {
|
|
312
|
+
// Handle typed input system
|
|
313
|
+
if (action.label_type) {
|
|
314
|
+
switch (action.label_type) {
|
|
315
|
+
case 'str':
|
|
316
|
+
return action.label || '';
|
|
317
|
+
|
|
318
|
+
case 'msg':
|
|
319
|
+
if (action.label) {
|
|
320
|
+
const msgData = this.messages[this.id] || {};
|
|
321
|
+
return this.getNestedProperty(msgData, action.label) || '';
|
|
322
|
+
}
|
|
323
|
+
return '';
|
|
324
|
+
|
|
325
|
+
case 'row':
|
|
326
|
+
if (action.label) {
|
|
327
|
+
return this.getNestedProperty(item, action.label) || '';
|
|
328
|
+
}
|
|
329
|
+
return '';
|
|
330
|
+
|
|
331
|
+
case 'jsonata':
|
|
332
|
+
// For JSONata, check if we have the expression from backend
|
|
333
|
+
const msgData = this.messages[this.id] || {};
|
|
334
|
+
const actionIndex = this.actions.indexOf(action);
|
|
335
|
+
|
|
336
|
+
if (msgData._jsonataLabels && msgData._jsonataLabels[actionIndex]) {
|
|
337
|
+
const result = this.evaluateJsonata(msgData._jsonataLabels[actionIndex], msgData, item);
|
|
338
|
+
return result || '';
|
|
339
|
+
}
|
|
340
|
+
return action.label || '';
|
|
341
|
+
|
|
342
|
+
default:
|
|
343
|
+
return action.label || '';
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Fallback to legacy handling for backward compatibility
|
|
348
|
+
const legacy = this.evaluateLegacyLabel(action, item);
|
|
349
|
+
return legacy || action.label || '';
|
|
350
|
+
},
|
|
351
|
+
|
|
352
|
+
getNestedProperty(obj, path) {
|
|
353
|
+
if (!path) return '';
|
|
354
|
+
|
|
355
|
+
const keys = path.split('.');
|
|
356
|
+
let current = obj;
|
|
357
|
+
|
|
358
|
+
for (const key of keys) {
|
|
359
|
+
if (current && typeof current === 'object' && key in current) {
|
|
360
|
+
current = current[key];
|
|
361
|
+
} else {
|
|
362
|
+
return path; // Return original path as fallback
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return current !== undefined && current !== null ? String(current) : path;
|
|
367
|
+
},
|
|
368
|
+
|
|
369
|
+
evaluateJsonata(expression, msgData, rowData) {
|
|
370
|
+
try {
|
|
371
|
+
// Create a unique key for this evaluation using expression and a hash of row data
|
|
372
|
+
const rowHash = JSON.stringify(rowData).slice(0, 50); // Truncate for performance
|
|
373
|
+
const key = `${expression}_${rowHash}`;
|
|
374
|
+
|
|
375
|
+
// Check if we already have a result for this combination
|
|
376
|
+
if (this.jsonataResults.has(key)) {
|
|
377
|
+
return this.jsonataResults.get(key);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Create a combined context with both message and row data
|
|
381
|
+
const context = {
|
|
382
|
+
...msgData,
|
|
383
|
+
$: rowData,
|
|
384
|
+
payload: msgData.payload,
|
|
385
|
+
msg: msgData,
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
const expr = jsonata(expression);
|
|
389
|
+
const result = expr.evaluate(context);
|
|
390
|
+
|
|
391
|
+
// Handle Promise return from JSONata
|
|
392
|
+
if (result && typeof result.then === 'function') {
|
|
393
|
+
// Store a pending indicator first
|
|
394
|
+
this.jsonataResults.set(key, expression);
|
|
395
|
+
|
|
396
|
+
// Handle the Promise
|
|
397
|
+
result
|
|
398
|
+
.then((resolvedValue) => {
|
|
399
|
+
const finalValue =
|
|
400
|
+
resolvedValue !== undefined && resolvedValue !== null ? String(resolvedValue) : '';
|
|
401
|
+
this.jsonataResults.set(key, finalValue);
|
|
402
|
+
this.$forceUpdate();
|
|
403
|
+
})
|
|
404
|
+
.catch((error) => {
|
|
405
|
+
console.warn('JSONata Promise evaluation error:', error.message);
|
|
406
|
+
this.jsonataResults.set(key, expression);
|
|
407
|
+
this.$forceUpdate();
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
return expression; // Return expression as placeholder
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Synchronous result
|
|
414
|
+
const finalValue = result !== undefined && result !== null ? String(result) : '';
|
|
415
|
+
this.jsonataResults.set(key, finalValue);
|
|
416
|
+
return finalValue;
|
|
417
|
+
} catch (error) {
|
|
418
|
+
console.warn('JSONata evaluation error:', error.message);
|
|
419
|
+
return expression; // fallback to original expression
|
|
420
|
+
}
|
|
421
|
+
},
|
|
422
|
+
|
|
423
|
+
evaluateLegacyLabel(action, item) {
|
|
424
|
+
// Legacy support for old "msg." and "row." prefixes
|
|
425
|
+
if (action.label && (action.label.startsWith('msg.') || action.label.startsWith('row.'))) {
|
|
426
|
+
let sourceObject;
|
|
427
|
+
let path;
|
|
428
|
+
|
|
429
|
+
if (action.label.startsWith('msg.')) {
|
|
430
|
+
sourceObject = this.messages[this.id] || {};
|
|
431
|
+
path = action.label.substring(4).split('.');
|
|
432
|
+
} else if (action.label.startsWith('row.')) {
|
|
433
|
+
sourceObject = item;
|
|
434
|
+
path = action.label.substring(4).split('.');
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Navigate through the object using the path
|
|
438
|
+
let value = sourceObject;
|
|
439
|
+
for (const key of path) {
|
|
440
|
+
if (value && typeof value === 'object' && key in value) {
|
|
441
|
+
value = value[key];
|
|
442
|
+
} else {
|
|
443
|
+
// Path not found, return original label as fallback
|
|
444
|
+
return action.label;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
return value !== undefined && value !== null ? String(value) : action.label;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
return action.label || '';
|
|
150
452
|
},
|
|
151
453
|
conditionCheck(condition, row) {
|
|
152
|
-
if (condition
|
|
454
|
+
if (!condition || condition === '') return true;
|
|
153
455
|
try {
|
|
154
456
|
const func = new Function('row', `return ${condition}`);
|
|
155
457
|
return func(row);
|
|
@@ -161,15 +463,18 @@ export default {
|
|
|
161
463
|
reloadData() {
|
|
162
464
|
this.isReloading = true;
|
|
163
465
|
|
|
164
|
-
this.send(
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
466
|
+
this.send(
|
|
467
|
+
{
|
|
468
|
+
_client: {
|
|
469
|
+
socketId: this.$socket.id,
|
|
470
|
+
widgetId: this.id,
|
|
471
|
+
},
|
|
472
|
+
payload: {
|
|
473
|
+
action: 'reload',
|
|
474
|
+
},
|
|
171
475
|
},
|
|
172
|
-
|
|
476
|
+
0
|
|
477
|
+
);
|
|
173
478
|
|
|
174
479
|
setTimeout(() => {
|
|
175
480
|
this.isReloading = false;
|
|
@@ -184,14 +489,16 @@ export default {
|
|
|
184
489
|
delete currentQuery[this.searchParamKey];
|
|
185
490
|
}
|
|
186
491
|
|
|
187
|
-
this.$router
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
492
|
+
this.$router
|
|
493
|
+
.replace({
|
|
494
|
+
path: this.$route.path,
|
|
495
|
+
query: currentQuery,
|
|
496
|
+
})
|
|
497
|
+
.catch((err) => {
|
|
498
|
+
if (err.name !== 'NavigationDuplicated') {
|
|
499
|
+
console.error('Router navigation error:', err);
|
|
500
|
+
}
|
|
501
|
+
});
|
|
195
502
|
}, 500),
|
|
196
503
|
loadSearchFromUrl() {
|
|
197
504
|
const searchValue = this.$route.query[this.searchParamKey];
|
|
@@ -245,12 +552,84 @@ export default {
|
|
|
245
552
|
this.loadSearchFromUrl();
|
|
246
553
|
}
|
|
247
554
|
},
|
|
248
|
-
deep: true
|
|
249
|
-
}
|
|
555
|
+
deep: true,
|
|
556
|
+
},
|
|
250
557
|
},
|
|
251
558
|
};
|
|
252
559
|
</script>
|
|
253
560
|
|
|
254
561
|
<style scoped>
|
|
255
562
|
@import '../stylesheets/ui-dynamic-table.css';
|
|
563
|
+
|
|
564
|
+
/* Search bar responsive styles */
|
|
565
|
+
.search-input {
|
|
566
|
+
margin-left: 12px;
|
|
567
|
+
margin-right: 50%;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
.mobile-search {
|
|
571
|
+
margin-right: 12px !important;
|
|
572
|
+
flex: 1;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/* Mobile Cards Styles */
|
|
576
|
+
.mobile-cards-container {
|
|
577
|
+
padding: 8px;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
.mobile-card {
|
|
581
|
+
margin-bottom: 12px !important;
|
|
582
|
+
border-radius: 8px;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
.mobile-card-row {
|
|
586
|
+
display: flex;
|
|
587
|
+
justify-content: space-between;
|
|
588
|
+
align-items: center;
|
|
589
|
+
padding: 4px 0;
|
|
590
|
+
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
.mobile-card-row:last-child {
|
|
594
|
+
border-bottom: none;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
.mobile-card-label {
|
|
598
|
+
font-weight: 600;
|
|
599
|
+
color: rgba(0, 0, 0, 0.7);
|
|
600
|
+
flex: 0 0 40%;
|
|
601
|
+
font-size: 0.875rem;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
.mobile-card-value {
|
|
605
|
+
flex: 1;
|
|
606
|
+
text-align: right;
|
|
607
|
+
word-break: break-word;
|
|
608
|
+
font-size: 0.875rem;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/* Responsive adjustments */
|
|
612
|
+
@media (max-width: 600px) {
|
|
613
|
+
.mobile-cards-container {
|
|
614
|
+
padding: 4px;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
.mobile-card {
|
|
618
|
+
margin-bottom: 8px !important;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
.mobile-card-label {
|
|
622
|
+
flex: 0 0 45%;
|
|
623
|
+
font-size: 0.8rem;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
.mobile-card-value {
|
|
627
|
+
font-size: 0.8rem;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
.search-input {
|
|
631
|
+
margin-left: 8px;
|
|
632
|
+
margin-right: 8px;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
256
635
|
</style>
|