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