@groundbrick/svelte-ui 0.1.1
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 +125 -0
- package/dist/components/Alert.svelte +335 -0
- package/dist/components/Alert.svelte.d.ts +24 -0
- package/dist/components/AutocompleteInput.svelte +356 -0
- package/dist/components/AutocompleteInput.svelte.d.ts +72 -0
- package/dist/components/Badge.svelte +185 -0
- package/dist/components/Badge.svelte.d.ts +20 -0
- package/dist/components/Button.svelte +415 -0
- package/dist/components/Button.svelte.d.ts +34 -0
- package/dist/components/Card.svelte +181 -0
- package/dist/components/Card.svelte.d.ts +24 -0
- package/dist/components/CardBody.svelte +78 -0
- package/dist/components/CardBody.svelte.d.ts +12 -0
- package/dist/components/CardFooter.svelte +81 -0
- package/dist/components/CardFooter.svelte.d.ts +14 -0
- package/dist/components/CardHeader.svelte +186 -0
- package/dist/components/CardHeader.svelte.d.ts +21 -0
- package/dist/components/Col.svelte +172 -0
- package/dist/components/Col.svelte.d.ts +26 -0
- package/dist/components/Container.svelte +118 -0
- package/dist/components/Container.svelte.d.ts +14 -0
- package/dist/components/Drawer.svelte +233 -0
- package/dist/components/Drawer.svelte.d.ts +13 -0
- package/dist/components/Dropdown.svelte +190 -0
- package/dist/components/Dropdown.svelte.d.ts +26 -0
- package/dist/components/DropdownItem.svelte +103 -0
- package/dist/components/DropdownItem.svelte.d.ts +22 -0
- package/dist/components/DurationInput.svelte +170 -0
- package/dist/components/DurationInput.svelte.d.ts +27 -0
- package/dist/components/EditableTable.svelte +647 -0
- package/dist/components/EditableTable.svelte.d.ts +74 -0
- package/dist/components/EmptyState.svelte +192 -0
- package/dist/components/EmptyState.svelte.d.ts +22 -0
- package/dist/components/FormField.svelte +260 -0
- package/dist/components/FormField.svelte.d.ts +68 -0
- package/dist/components/GridView.svelte +1022 -0
- package/dist/components/GridView.svelte.d.ts +38 -0
- package/dist/components/GridView.types.d.ts +28 -0
- package/dist/components/GridView.types.js +1 -0
- package/dist/components/LoadingSpinner.svelte +253 -0
- package/dist/components/LoadingSpinner.svelte.d.ts +17 -0
- package/dist/components/Modal.svelte +473 -0
- package/dist/components/Modal.svelte.d.ts +42 -0
- package/dist/components/PhoneInput.svelte +406 -0
- package/dist/components/PhoneInput.svelte.d.ts +31 -0
- package/dist/components/PhotoUpload.svelte +529 -0
- package/dist/components/PhotoUpload.svelte.d.ts +46 -0
- package/dist/components/Row.svelte +153 -0
- package/dist/components/Row.svelte.d.ts +18 -0
- package/dist/icons/PawPrintIcon.svelte +41 -0
- package/dist/icons/PawPrintIcon.svelte.d.ts +14 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.js +49 -0
- package/dist/styles/forms.css +182 -0
- package/dist/styles/tokens.css +243 -0
- package/dist/utils/duration.d.ts +20 -0
- package/dist/utils/duration.js +40 -0
- package/dist/utils/scrollLock.d.ts +7 -0
- package/dist/utils/scrollLock.js +26 -0
- package/package.json +66 -0
|
@@ -0,0 +1,1022 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { ActionItem, Column } from "./GridView.types.js";
|
|
3
|
+
|
|
4
|
+
// Browser guard without SvelteKit's $app/environment.
|
|
5
|
+
const browser = typeof window !== "undefined";
|
|
6
|
+
|
|
7
|
+
/** Default date formatter; override via the `formatDate` prop. */
|
|
8
|
+
function defaultFormatDate(value: any): string {
|
|
9
|
+
if (!value) return "-";
|
|
10
|
+
const d = new Date(value);
|
|
11
|
+
return isNaN(d.getTime()) ? String(value) : d.toLocaleDateString("pt-PT");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Props
|
|
15
|
+
export let data: any[] = [];
|
|
16
|
+
export let columns: Column[] = [];
|
|
17
|
+
export let loading: boolean = false;
|
|
18
|
+
export let emptyMessage: string = "Nenhum item encontrado";
|
|
19
|
+
export let searchable: boolean = false;
|
|
20
|
+
export let searchPlaceholder: string = "Pesquisar...";
|
|
21
|
+
export let selectable: boolean = false;
|
|
22
|
+
export let draggable: boolean = false;
|
|
23
|
+
export let onRowClick: ((row: any) => void) | undefined = undefined;
|
|
24
|
+
export let onSelectionChange: ((selectedRows: any[]) => void) | undefined = undefined;
|
|
25
|
+
export let onReorder: ((orderedIds: number[]) => Promise<void>) | undefined = undefined;
|
|
26
|
+
export let actions: ActionItem[] = [];
|
|
27
|
+
// Opt-in client-side pagination
|
|
28
|
+
export let paginated: boolean = false;
|
|
29
|
+
export let pageSize: number = 25;
|
|
30
|
+
// Opt-in persistence of search/sort/page to sessionStorage (survives back navigation)
|
|
31
|
+
export let persistKey: string | undefined = undefined;
|
|
32
|
+
// Dependency-injection props (replace app-specific utilities):
|
|
33
|
+
/** Resolve an image cell value to a URL. Defaults to identity (use value as-is). */
|
|
34
|
+
export let resolveImageUrl: (value: string, folder?: string) => string = (value) => value;
|
|
35
|
+
/** Format a date cell value. Defaults to pt-PT locale date. */
|
|
36
|
+
export let formatDate: (value: any) => string = defaultFormatDate;
|
|
37
|
+
/** Permission check for row actions. Defaults to allow-all. */
|
|
38
|
+
export let hasPermission: (permission: string) => boolean = () => true;
|
|
39
|
+
|
|
40
|
+
// Drag and drop state
|
|
41
|
+
let draggedIndex: number | null = null;
|
|
42
|
+
let dropTargetIndex: number | null = null;
|
|
43
|
+
let isDragging = false;
|
|
44
|
+
let localData: any[] = [];
|
|
45
|
+
let dataSignature = '';
|
|
46
|
+
|
|
47
|
+
// Long press state for touch devices
|
|
48
|
+
let longPressTimer: ReturnType<typeof setTimeout> | null = null;
|
|
49
|
+
let longPressingIndex: number | null = null;
|
|
50
|
+
const LONG_PRESS_DURATION = 500; // ms
|
|
51
|
+
|
|
52
|
+
// Create a signature from data IDs to detect actual data changes vs re-renders
|
|
53
|
+
function getDataSignature(arr: any[]): string {
|
|
54
|
+
return JSON.stringify(arr);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Only sync localData with data prop when actual data changes (not during drag)
|
|
58
|
+
$: {
|
|
59
|
+
const newSignature = getDataSignature(data);
|
|
60
|
+
if (newSignature !== dataSignature && !isDragging) {
|
|
61
|
+
localData = [...data];
|
|
62
|
+
dataSignature = newSignature;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// State
|
|
67
|
+
let searchQuery = "";
|
|
68
|
+
let sortColumn: string | null = null;
|
|
69
|
+
let sortDirection: "asc" | "desc" = "asc";
|
|
70
|
+
let currentPage = 1;
|
|
71
|
+
let selectedRows = new Set<any>();
|
|
72
|
+
|
|
73
|
+
// Restore persisted view state (search / sort / page) before reactivity kicks in
|
|
74
|
+
const STORAGE_PREFIX = "gridview:";
|
|
75
|
+
if (browser && persistKey) {
|
|
76
|
+
try {
|
|
77
|
+
const saved = sessionStorage.getItem(STORAGE_PREFIX + persistKey);
|
|
78
|
+
if (saved) {
|
|
79
|
+
const s = JSON.parse(saved);
|
|
80
|
+
searchQuery = s.searchQuery ?? "";
|
|
81
|
+
sortColumn = s.sortColumn ?? null;
|
|
82
|
+
sortDirection = s.sortDirection ?? "asc";
|
|
83
|
+
currentPage = s.currentPage ?? 1;
|
|
84
|
+
}
|
|
85
|
+
} catch {
|
|
86
|
+
// ignore corrupted storage
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Reset to first page whenever the search term changes (init aligned to restored value)
|
|
91
|
+
let lastSearchQuery = searchQuery;
|
|
92
|
+
$: if (searchQuery !== lastSearchQuery) {
|
|
93
|
+
lastSearchQuery = searchQuery;
|
|
94
|
+
currentPage = 1;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Persist view state
|
|
98
|
+
$: if (browser && persistKey) {
|
|
99
|
+
sessionStorage.setItem(
|
|
100
|
+
STORAGE_PREFIX + persistKey,
|
|
101
|
+
JSON.stringify({ searchQuery, sortColumn, sortDirection, currentPage }),
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Reactive statements
|
|
106
|
+
$: filteredData = (() => {
|
|
107
|
+
let result = [...localData];
|
|
108
|
+
|
|
109
|
+
// Search filter
|
|
110
|
+
if (searchable && searchQuery.trim()) {
|
|
111
|
+
const query = searchQuery.toLowerCase();
|
|
112
|
+
result = result.filter((row) =>
|
|
113
|
+
columns.some((col) => {
|
|
114
|
+
const value = row[col.key];
|
|
115
|
+
return value && value.toString().toLowerCase().includes(query);
|
|
116
|
+
}),
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Sort
|
|
121
|
+
if (sortColumn) {
|
|
122
|
+
result.sort((a, b) => {
|
|
123
|
+
const aVal = a[sortColumn!];
|
|
124
|
+
const bVal = b[sortColumn!];
|
|
125
|
+
|
|
126
|
+
let comparison = 0;
|
|
127
|
+
if (aVal < bVal) comparison = -1;
|
|
128
|
+
else if (aVal > bVal) comparison = 1;
|
|
129
|
+
|
|
130
|
+
return sortDirection === "asc" ? comparison : -comparison;
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return result;
|
|
135
|
+
})();
|
|
136
|
+
|
|
137
|
+
// Pagination (client-side, opt-in)
|
|
138
|
+
$: totalPages = paginated
|
|
139
|
+
? Math.max(1, Math.ceil(filteredData.length / pageSize))
|
|
140
|
+
: 1;
|
|
141
|
+
$: if (currentPage > totalPages) currentPage = totalPages;
|
|
142
|
+
$: displayData = paginated
|
|
143
|
+
? filteredData.slice((currentPage - 1) * pageSize, currentPage * pageSize)
|
|
144
|
+
: filteredData;
|
|
145
|
+
|
|
146
|
+
// Windowed page buttons with ellipsis markers (-1)
|
|
147
|
+
$: pageNumbers = (() => {
|
|
148
|
+
if (!paginated || totalPages <= 1) return [] as number[];
|
|
149
|
+
const pages = new Set<number>([1, totalPages, currentPage]);
|
|
150
|
+
if (currentPage - 1 > 1) pages.add(currentPage - 1);
|
|
151
|
+
if (currentPage + 1 < totalPages) pages.add(currentPage + 1);
|
|
152
|
+
const sorted = Array.from(pages).sort((a, b) => a - b);
|
|
153
|
+
const result: number[] = [];
|
|
154
|
+
let prev = 0;
|
|
155
|
+
for (const p of sorted) {
|
|
156
|
+
if (prev && p - prev > 1) result.push(-1); // ellipsis
|
|
157
|
+
result.push(p);
|
|
158
|
+
prev = p;
|
|
159
|
+
}
|
|
160
|
+
return result;
|
|
161
|
+
})();
|
|
162
|
+
|
|
163
|
+
$: rangeStart = filteredData.length === 0 ? 0 : (currentPage - 1) * pageSize + 1;
|
|
164
|
+
$: rangeEnd = paginated
|
|
165
|
+
? Math.min(currentPage * pageSize, filteredData.length)
|
|
166
|
+
: filteredData.length;
|
|
167
|
+
|
|
168
|
+
function goToPage(p: number) {
|
|
169
|
+
if (p < 1 || p > totalPages) return;
|
|
170
|
+
currentPage = p;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function clearSearch() {
|
|
174
|
+
searchQuery = "";
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function handleSearchKeydown(e: KeyboardEvent) {
|
|
178
|
+
if (e.key === "Escape") {
|
|
179
|
+
e.preventDefault();
|
|
180
|
+
clearSearch();
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Build grid template columns
|
|
185
|
+
$: gridTemplateColumns = (() => {
|
|
186
|
+
const cols: string[] = [];
|
|
187
|
+
if (draggable) cols.push('40px');
|
|
188
|
+
if (selectable) cols.push('50px');
|
|
189
|
+
columns.forEach(col => {
|
|
190
|
+
if (!col.hidden) cols.push(col.width || '1fr');
|
|
191
|
+
});
|
|
192
|
+
if (actionsAvailable) cols.push('120px');
|
|
193
|
+
return cols.join(' ');
|
|
194
|
+
})();
|
|
195
|
+
|
|
196
|
+
// Methods
|
|
197
|
+
function handleSort(column: Column) {
|
|
198
|
+
if (!column.sortable) return;
|
|
199
|
+
|
|
200
|
+
if (sortColumn === column.key) {
|
|
201
|
+
sortDirection = sortDirection === "asc" ? "desc" : "asc";
|
|
202
|
+
} else {
|
|
203
|
+
sortColumn = column.key;
|
|
204
|
+
sortDirection = "asc";
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function handleRowClick(row: any) {
|
|
209
|
+
if (onRowClick) {
|
|
210
|
+
onRowClick(row);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function toggleRowSelection(row: any) {
|
|
215
|
+
if (selectedRows.has(row)) {
|
|
216
|
+
selectedRows.delete(row);
|
|
217
|
+
} else {
|
|
218
|
+
selectedRows.add(row);
|
|
219
|
+
}
|
|
220
|
+
selectedRows = new Set(selectedRows);
|
|
221
|
+
if (onSelectionChange) {
|
|
222
|
+
onSelectionChange(Array.from(selectedRows));
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function toggleSelectAll() {
|
|
227
|
+
if (selectedRows.size === filteredData.length) {
|
|
228
|
+
selectedRows.clear();
|
|
229
|
+
} else {
|
|
230
|
+
selectedRows = new Set(filteredData);
|
|
231
|
+
}
|
|
232
|
+
selectedRows = new Set(selectedRows);
|
|
233
|
+
if (onSelectionChange) {
|
|
234
|
+
onSelectionChange(Array.from(selectedRows));
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function formatCellValue(value: any, column: Column, row: any): string {
|
|
239
|
+
if (column.customRender) {
|
|
240
|
+
return column.customRender(value, row);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (!value) return "-";
|
|
244
|
+
|
|
245
|
+
switch (column.type) {
|
|
246
|
+
case "date":
|
|
247
|
+
return formatDate(value);
|
|
248
|
+
case "number":
|
|
249
|
+
return Number(value).toLocaleString("pt-PT");
|
|
250
|
+
default:
|
|
251
|
+
return value.toString();
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function resolveRowActions(row: any): ActionItem[] {
|
|
256
|
+
return actions.filter((action) => {
|
|
257
|
+
if (action.permission && !hasPermission(action.permission)) {
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (action.condition && !action.condition(row)) {
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return true;
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
let actionsAvailable = false;
|
|
270
|
+
$: actionsAvailable = filteredData.some((row) => resolveRowActions(row).length > 0);
|
|
271
|
+
|
|
272
|
+
// Drag and drop handlers - only triggered from drag handle
|
|
273
|
+
function handleDragStart(e: DragEvent, index: number) {
|
|
274
|
+
if (!draggable) return;
|
|
275
|
+
draggedIndex = index;
|
|
276
|
+
isDragging = true;
|
|
277
|
+
if (e.dataTransfer) {
|
|
278
|
+
e.dataTransfer.effectAllowed = 'move';
|
|
279
|
+
e.dataTransfer.setData('text/plain', String(index));
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function handleDragOver(e: DragEvent, index: number) {
|
|
284
|
+
if (!draggable || draggedIndex === null) return;
|
|
285
|
+
e.preventDefault();
|
|
286
|
+
if (e.dataTransfer) {
|
|
287
|
+
e.dataTransfer.dropEffect = 'move';
|
|
288
|
+
}
|
|
289
|
+
dropTargetIndex = index;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function handleDragLeave() {
|
|
293
|
+
dropTargetIndex = null;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function handleDrop(e: DragEvent, index: number) {
|
|
297
|
+
if (!draggable || draggedIndex === null) return;
|
|
298
|
+
e.preventDefault();
|
|
299
|
+
|
|
300
|
+
if (draggedIndex !== index) {
|
|
301
|
+
// Reorder the local data array
|
|
302
|
+
const newData = [...localData];
|
|
303
|
+
const [removed] = newData.splice(draggedIndex, 1);
|
|
304
|
+
newData.splice(index, 0, removed);
|
|
305
|
+
|
|
306
|
+
// Update local data immediately for visual feedback
|
|
307
|
+
localData = newData;
|
|
308
|
+
|
|
309
|
+
// Update signature to reflect new order
|
|
310
|
+
dataSignature = getDataSignature(newData);
|
|
311
|
+
|
|
312
|
+
// Get ordered IDs and call the callback
|
|
313
|
+
const orderedIds = newData.map(item => item.id);
|
|
314
|
+
if (onReorder) {
|
|
315
|
+
onReorder(orderedIds);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
draggedIndex = null;
|
|
320
|
+
dropTargetIndex = null;
|
|
321
|
+
isDragging = false;
|
|
322
|
+
longPressingIndex = null;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function handleDragEnd() {
|
|
326
|
+
draggedIndex = null;
|
|
327
|
+
dropTargetIndex = null;
|
|
328
|
+
isDragging = false;
|
|
329
|
+
longPressingIndex = null;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Long press handlers for touch devices
|
|
333
|
+
function startLongPress(e: TouchEvent, index: number) {
|
|
334
|
+
if (!draggable) return;
|
|
335
|
+
e.preventDefault();
|
|
336
|
+
|
|
337
|
+
if (longPressTimer) clearTimeout(longPressTimer);
|
|
338
|
+
longPressingIndex = index;
|
|
339
|
+
|
|
340
|
+
longPressTimer = setTimeout(() => {
|
|
341
|
+
// After long press, initiate drag mode
|
|
342
|
+
draggedIndex = index;
|
|
343
|
+
isDragging = true;
|
|
344
|
+
// Vibrate if available for haptic feedback
|
|
345
|
+
if (navigator.vibrate) {
|
|
346
|
+
navigator.vibrate(50);
|
|
347
|
+
}
|
|
348
|
+
}, LONG_PRESS_DURATION);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function cancelLongPress() {
|
|
352
|
+
if (longPressTimer) {
|
|
353
|
+
clearTimeout(longPressTimer);
|
|
354
|
+
longPressTimer = null;
|
|
355
|
+
}
|
|
356
|
+
if (!isDragging) {
|
|
357
|
+
longPressingIndex = null;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function handleTouchMove(e: TouchEvent, index: number) {
|
|
362
|
+
if (!isDragging || draggedIndex === null) {
|
|
363
|
+
cancelLongPress();
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Find which row we're over
|
|
368
|
+
const touch = e.touches[0];
|
|
369
|
+
const element = document.elementFromPoint(touch.clientX, touch.clientY);
|
|
370
|
+
const row = element?.closest('.grid-row');
|
|
371
|
+
if (row) {
|
|
372
|
+
const rowIndex = parseInt(row.getAttribute('data-index') || '-1');
|
|
373
|
+
if (rowIndex >= 0) {
|
|
374
|
+
dropTargetIndex = rowIndex;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function handleTouchEnd(e: TouchEvent, index: number) {
|
|
380
|
+
cancelLongPress();
|
|
381
|
+
|
|
382
|
+
if (isDragging && draggedIndex !== null && dropTargetIndex !== null) {
|
|
383
|
+
// Perform the reorder
|
|
384
|
+
if (draggedIndex !== dropTargetIndex) {
|
|
385
|
+
const newData = [...localData];
|
|
386
|
+
const [removed] = newData.splice(draggedIndex, 1);
|
|
387
|
+
newData.splice(dropTargetIndex, 0, removed);
|
|
388
|
+
localData = newData;
|
|
389
|
+
dataSignature = getDataSignature(newData);
|
|
390
|
+
const orderedIds = newData.map(item => item.id);
|
|
391
|
+
if (onReorder) {
|
|
392
|
+
onReorder(orderedIds);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
draggedIndex = null;
|
|
398
|
+
dropTargetIndex = null;
|
|
399
|
+
isDragging = false;
|
|
400
|
+
longPressingIndex = null;
|
|
401
|
+
}
|
|
402
|
+
</script>
|
|
403
|
+
|
|
404
|
+
<div class="grid-view">
|
|
405
|
+
{#if searchable}
|
|
406
|
+
<div class="card mb-3">
|
|
407
|
+
<div class="card-body">
|
|
408
|
+
<div class="row align-items-center">
|
|
409
|
+
<div class="col-md-6">
|
|
410
|
+
<label for="grid-search" class="visually-hidden">Pesquisar na tabela</label>
|
|
411
|
+
<div class="search-group">
|
|
412
|
+
<input
|
|
413
|
+
id="grid-search"
|
|
414
|
+
bind:value={searchQuery}
|
|
415
|
+
on:keydown={handleSearchKeydown}
|
|
416
|
+
type="search"
|
|
417
|
+
class="form-control"
|
|
418
|
+
placeholder={searchPlaceholder}
|
|
419
|
+
aria-label="Pesquisar na tabela"
|
|
420
|
+
/>
|
|
421
|
+
{#if searchQuery}
|
|
422
|
+
<button
|
|
423
|
+
type="button"
|
|
424
|
+
class="btn btn-clear-search"
|
|
425
|
+
on:click={clearSearch}
|
|
426
|
+
aria-label="Limpar pesquisa"
|
|
427
|
+
>
|
|
428
|
+
<i class="bi bi-x-lg"></i>
|
|
429
|
+
Limpar
|
|
430
|
+
</button>
|
|
431
|
+
{/if}
|
|
432
|
+
</div>
|
|
433
|
+
</div>
|
|
434
|
+
<div class="col-md-6 text-end">
|
|
435
|
+
<small class="text-muted">
|
|
436
|
+
{#if paginated && filteredData.length > 0}
|
|
437
|
+
{rangeStart}–{rangeEnd} de {filteredData.length} registos
|
|
438
|
+
{:else}
|
|
439
|
+
{filteredData.length} de {data.length} registos
|
|
440
|
+
{/if}
|
|
441
|
+
{#if selectedRows.size > 0}
|
|
442
|
+
• {selectedRows.size} selecionados
|
|
443
|
+
{/if}
|
|
444
|
+
</small>
|
|
445
|
+
</div>
|
|
446
|
+
</div>
|
|
447
|
+
</div>
|
|
448
|
+
</div>
|
|
449
|
+
{/if}
|
|
450
|
+
|
|
451
|
+
<div class="card">
|
|
452
|
+
{#if loading}
|
|
453
|
+
<div class="card-body text-center py-5">
|
|
454
|
+
<div class="spinner-border" role="status">
|
|
455
|
+
<span class="visually-hidden">A carregar...</span>
|
|
456
|
+
</div>
|
|
457
|
+
<p class="mt-2 text-muted">A carregar dados...</p>
|
|
458
|
+
</div>
|
|
459
|
+
{:else if filteredData.length === 0}
|
|
460
|
+
<div class="card-body text-center py-5">
|
|
461
|
+
<div class="text-muted">
|
|
462
|
+
<i class="bi bi-inbox" style="font-size: 3rem;"></i>
|
|
463
|
+
<h5 class="mt-3">{emptyMessage}</h5>
|
|
464
|
+
{#if searchQuery}
|
|
465
|
+
<p>Nenhum resultado encontrado para "{searchQuery}"</p>
|
|
466
|
+
{/if}
|
|
467
|
+
</div>
|
|
468
|
+
</div>
|
|
469
|
+
{:else}
|
|
470
|
+
<div class="grid-table-wrapper">
|
|
471
|
+
<div class="grid-table">
|
|
472
|
+
<!-- Header -->
|
|
473
|
+
<div class="grid-header" style="grid-template-columns: {gridTemplateColumns};">
|
|
474
|
+
{#if draggable}
|
|
475
|
+
<div class="grid-cell header-cell"></div>
|
|
476
|
+
{/if}
|
|
477
|
+
{#if selectable}
|
|
478
|
+
<div class="grid-cell header-cell">
|
|
479
|
+
<input
|
|
480
|
+
type="checkbox"
|
|
481
|
+
class="form-check-input"
|
|
482
|
+
checked={selectedRows.size === filteredData.length && filteredData.length > 0}
|
|
483
|
+
indeterminate={selectedRows.size > 0 && selectedRows.size < filteredData.length}
|
|
484
|
+
on:change={toggleSelectAll}
|
|
485
|
+
/>
|
|
486
|
+
</div>
|
|
487
|
+
{/if}
|
|
488
|
+
{#each columns as column}
|
|
489
|
+
{#if !column.hidden}
|
|
490
|
+
<div
|
|
491
|
+
class="grid-cell header-cell"
|
|
492
|
+
class:sortable={column.sortable}
|
|
493
|
+
on:click={() => handleSort(column)}
|
|
494
|
+
on:keydown={(e) => e.key === 'Enter' && handleSort(column)}
|
|
495
|
+
role={column.sortable ? "button" : undefined}
|
|
496
|
+
tabindex={column.sortable ? 0 : undefined}
|
|
497
|
+
>
|
|
498
|
+
{column.title}
|
|
499
|
+
{#if column.sortable}
|
|
500
|
+
{#if sortColumn === column.key}
|
|
501
|
+
{#if sortDirection === "asc"}
|
|
502
|
+
<i class="bi bi-arrow-up"></i>
|
|
503
|
+
{:else}
|
|
504
|
+
<i class="bi bi-arrow-down"></i>
|
|
505
|
+
{/if}
|
|
506
|
+
{:else}
|
|
507
|
+
<i class="bi bi-arrow-down-up opacity-50"></i>
|
|
508
|
+
{/if}
|
|
509
|
+
{/if}
|
|
510
|
+
</div>
|
|
511
|
+
{/if}
|
|
512
|
+
{/each}
|
|
513
|
+
{#if actionsAvailable}
|
|
514
|
+
<div class="grid-cell header-cell">Ações</div>
|
|
515
|
+
{/if}
|
|
516
|
+
</div>
|
|
517
|
+
|
|
518
|
+
<!-- Body -->
|
|
519
|
+
<div class="grid-body">
|
|
520
|
+
{#each displayData as row, index (row.id ?? index)}
|
|
521
|
+
<div
|
|
522
|
+
class="grid-row"
|
|
523
|
+
class:clickable-row={onRowClick && !isDragging}
|
|
524
|
+
class:dragging={draggedIndex === index}
|
|
525
|
+
class:drop-above={dropTargetIndex === index && draggedIndex !== null && draggedIndex > index}
|
|
526
|
+
class:drop-below={dropTargetIndex === index && draggedIndex !== null && draggedIndex < index}
|
|
527
|
+
class:long-pressing={longPressingIndex === index && !isDragging}
|
|
528
|
+
style="grid-template-columns: {gridTemplateColumns};"
|
|
529
|
+
data-index={index}
|
|
530
|
+
on:click={() => !isDragging && handleRowClick(row)}
|
|
531
|
+
on:dragover={(e) => handleDragOver(e, index)}
|
|
532
|
+
on:dragleave={handleDragLeave}
|
|
533
|
+
on:drop={(e) => handleDrop(e, index)}
|
|
534
|
+
on:touchmove={(e) => handleTouchMove(e, index)}
|
|
535
|
+
on:touchend={(e) => handleTouchEnd(e, index)}
|
|
536
|
+
role="row"
|
|
537
|
+
>
|
|
538
|
+
{#if draggable}
|
|
539
|
+
<div
|
|
540
|
+
class="grid-cell drag-handle"
|
|
541
|
+
draggable="true"
|
|
542
|
+
on:dragstart={(e) => handleDragStart(e, index)}
|
|
543
|
+
on:dragend={handleDragEnd}
|
|
544
|
+
on:touchstart={(e) => startLongPress(e, index)}
|
|
545
|
+
on:touchend={cancelLongPress}
|
|
546
|
+
on:click={(e) => e.stopPropagation()}
|
|
547
|
+
>
|
|
548
|
+
<i class="bi bi-grip-vertical"></i>
|
|
549
|
+
</div>
|
|
550
|
+
{/if}
|
|
551
|
+
{#if selectable}
|
|
552
|
+
<div class="grid-cell" on:click={(e) => e.stopPropagation()}>
|
|
553
|
+
<input
|
|
554
|
+
type="checkbox"
|
|
555
|
+
class="form-check-input"
|
|
556
|
+
checked={selectedRows.has(row)}
|
|
557
|
+
on:change={() => toggleRowSelection(row)}
|
|
558
|
+
/>
|
|
559
|
+
</div>
|
|
560
|
+
{/if}
|
|
561
|
+
{#each columns as column}
|
|
562
|
+
{#if !column.hidden}
|
|
563
|
+
<div class="grid-cell">
|
|
564
|
+
{#if column.type === "image"}
|
|
565
|
+
{#if row[column.key]}
|
|
566
|
+
<img src={resolveImageUrl(row[column.key], column.imageFolder || 'pets')} alt="Imagem" class="table-image" />
|
|
567
|
+
{:else}
|
|
568
|
+
<div class="table-image-placeholder">
|
|
569
|
+
<i class="bi bi-image"></i>
|
|
570
|
+
</div>
|
|
571
|
+
{/if}
|
|
572
|
+
{:else if column.type === "custom" && column.component}
|
|
573
|
+
{@const props = column.componentProps ? column.componentProps(row[column.key], row) : { value: row[column.key] }}
|
|
574
|
+
<svelte:component this={column.component} {...props} />
|
|
575
|
+
{:else if column.type === "badge"}
|
|
576
|
+
{@const variant = column.badgeVariant ? column.badgeVariant(row[column.key]) : 'secondary'}
|
|
577
|
+
<span class="badge bg-{variant}">
|
|
578
|
+
{formatCellValue(row[column.key], column, row)}
|
|
579
|
+
</span>
|
|
580
|
+
{:else if column.customRender}
|
|
581
|
+
{@html formatCellValue(row[column.key], column, row)}
|
|
582
|
+
{:else}
|
|
583
|
+
{formatCellValue(row[column.key], column, row)}
|
|
584
|
+
{/if}
|
|
585
|
+
</div>
|
|
586
|
+
{/if}
|
|
587
|
+
{/each}
|
|
588
|
+
{#if actionsAvailable}
|
|
589
|
+
{@const rowActions = resolveRowActions(row)}
|
|
590
|
+
<div class="grid-cell" on:click={(e) => e.stopPropagation()}>
|
|
591
|
+
{#if rowActions.length > 0}
|
|
592
|
+
<div class="dropdown">
|
|
593
|
+
<button
|
|
594
|
+
class="btn btn-sm btn-outline-secondary dropdown-toggle"
|
|
595
|
+
type="button"
|
|
596
|
+
data-bs-toggle="dropdown"
|
|
597
|
+
aria-label="Ações"
|
|
598
|
+
>
|
|
599
|
+
<i class="bi bi-three-dots"></i>
|
|
600
|
+
</button>
|
|
601
|
+
<ul class="dropdown-menu">
|
|
602
|
+
{#each rowActions as action}
|
|
603
|
+
<li>
|
|
604
|
+
<button
|
|
605
|
+
class="dropdown-item"
|
|
606
|
+
class:dropdown-item-success={action.variant === 'success'}
|
|
607
|
+
class:dropdown-item-danger={action.variant === 'danger'}
|
|
608
|
+
class:dropdown-item-warning={action.variant === 'warning'}
|
|
609
|
+
class:dropdown-item-info={action.variant === 'info'}
|
|
610
|
+
on:click={() => action.onClick(row)}
|
|
611
|
+
>
|
|
612
|
+
{#if action.icon}
|
|
613
|
+
<i class="bi {action.icon} me-2"></i>
|
|
614
|
+
{/if}
|
|
615
|
+
{action.label}
|
|
616
|
+
</button>
|
|
617
|
+
</li>
|
|
618
|
+
{/each}
|
|
619
|
+
</ul>
|
|
620
|
+
</div>
|
|
621
|
+
{/if}
|
|
622
|
+
</div>
|
|
623
|
+
{/if}
|
|
624
|
+
</div>
|
|
625
|
+
{/each}
|
|
626
|
+
</div>
|
|
627
|
+
</div>
|
|
628
|
+
</div>
|
|
629
|
+
{#if paginated && totalPages > 1}
|
|
630
|
+
<div class="grid-pagination">
|
|
631
|
+
<button
|
|
632
|
+
type="button"
|
|
633
|
+
class="page-btn page-nav"
|
|
634
|
+
disabled={currentPage === 1}
|
|
635
|
+
on:click={() => goToPage(currentPage - 1)}
|
|
636
|
+
aria-label="Página anterior"
|
|
637
|
+
>
|
|
638
|
+
<i class="bi bi-chevron-left"></i>
|
|
639
|
+
</button>
|
|
640
|
+
{#each pageNumbers as p}
|
|
641
|
+
{#if p === -1}
|
|
642
|
+
<span class="page-ellipsis">…</span>
|
|
643
|
+
{:else}
|
|
644
|
+
<button
|
|
645
|
+
type="button"
|
|
646
|
+
class="page-btn"
|
|
647
|
+
class:active={p === currentPage}
|
|
648
|
+
aria-current={p === currentPage ? "page" : undefined}
|
|
649
|
+
on:click={() => goToPage(p)}
|
|
650
|
+
>
|
|
651
|
+
{p}
|
|
652
|
+
</button>
|
|
653
|
+
{/if}
|
|
654
|
+
{/each}
|
|
655
|
+
<button
|
|
656
|
+
type="button"
|
|
657
|
+
class="page-btn page-nav"
|
|
658
|
+
disabled={currentPage === totalPages}
|
|
659
|
+
on:click={() => goToPage(currentPage + 1)}
|
|
660
|
+
aria-label="Página seguinte"
|
|
661
|
+
>
|
|
662
|
+
<i class="bi bi-chevron-right"></i>
|
|
663
|
+
</button>
|
|
664
|
+
</div>
|
|
665
|
+
{/if}
|
|
666
|
+
{/if}
|
|
667
|
+
</div>
|
|
668
|
+
</div>
|
|
669
|
+
|
|
670
|
+
<style>
|
|
671
|
+
/* Search input + clear button */
|
|
672
|
+
.search-group {
|
|
673
|
+
display: flex;
|
|
674
|
+
align-items: center;
|
|
675
|
+
gap: var(--spacing-sm);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
.search-group .form-control {
|
|
679
|
+
flex: 1 1 auto;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
.btn-clear-search {
|
|
683
|
+
flex: 0 0 auto;
|
|
684
|
+
display: inline-flex;
|
|
685
|
+
align-items: center;
|
|
686
|
+
gap: var(--spacing-xs);
|
|
687
|
+
white-space: nowrap;
|
|
688
|
+
border: 1px solid var(--color-border-subtle);
|
|
689
|
+
color: var(--color-text-muted);
|
|
690
|
+
background: var(--color-bg-surface);
|
|
691
|
+
font-size: var(--font-size-sm);
|
|
692
|
+
transition: all var(--transition-fast);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
.btn-clear-search:hover {
|
|
696
|
+
border-color: var(--color-primary);
|
|
697
|
+
color: var(--color-primary);
|
|
698
|
+
background: var(--color-primary-light);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
/* Grid Table Layout */
|
|
702
|
+
.grid-table-wrapper {
|
|
703
|
+
overflow-x: auto;
|
|
704
|
+
-webkit-overflow-scrolling: touch;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
.grid-table {
|
|
708
|
+
width: 100%;
|
|
709
|
+
min-width: 600px; /* Minimum width before scroll kicks in */
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
.grid-header {
|
|
713
|
+
display: grid;
|
|
714
|
+
background: var(--gradient-card-header);
|
|
715
|
+
border-bottom: 2px solid var(--color-primary-light);
|
|
716
|
+
/* Match the card's inner rounded corners so the gradient doesn't bleed past them */
|
|
717
|
+
border-top-left-radius: calc(var(--radius-lg) - 1px);
|
|
718
|
+
border-top-right-radius: calc(var(--radius-lg) - 1px);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
.grid-body {
|
|
722
|
+
display: flex;
|
|
723
|
+
flex-direction: column;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
.grid-row {
|
|
727
|
+
display: grid;
|
|
728
|
+
border-bottom: 1px solid var(--border-color-light);
|
|
729
|
+
transition: background-color 0.15s ease;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
.grid-row:last-child {
|
|
733
|
+
border-bottom: none;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
.grid-cell {
|
|
737
|
+
padding: var(--spacing-md) var(--spacing-lg);
|
|
738
|
+
display: flex;
|
|
739
|
+
align-items: center;
|
|
740
|
+
overflow: hidden;
|
|
741
|
+
text-overflow: ellipsis;
|
|
742
|
+
white-space: nowrap;
|
|
743
|
+
min-width: 0;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
/* Header styling */
|
|
747
|
+
.header-cell {
|
|
748
|
+
font-weight: var(--font-weight-semibold);
|
|
749
|
+
font-size: var(--font-size-sm);
|
|
750
|
+
color: var(--color-primary);
|
|
751
|
+
text-transform: uppercase;
|
|
752
|
+
letter-spacing: 0.03em;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
.header-cell.sortable {
|
|
756
|
+
cursor: pointer;
|
|
757
|
+
user-select: none;
|
|
758
|
+
transition: all var(--transition-fast);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
.header-cell.sortable:hover {
|
|
762
|
+
background-color: var(--color-primary-light);
|
|
763
|
+
color: var(--color-primary-active);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
.header-cell i {
|
|
767
|
+
font-size: var(--font-size-sm);
|
|
768
|
+
margin-left: var(--spacing-xs);
|
|
769
|
+
color: var(--color-gray-500);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
.header-cell i.bi-arrow-up,
|
|
773
|
+
.header-cell i.bi-arrow-down {
|
|
774
|
+
color: var(--color-primary);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
/* Row styling */
|
|
778
|
+
.clickable-row {
|
|
779
|
+
cursor: pointer;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
.clickable-row:hover {
|
|
783
|
+
background-color: var(--color-primary-light);
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
/* Drag and drop styling */
|
|
787
|
+
.drag-handle {
|
|
788
|
+
justify-content: center;
|
|
789
|
+
color: var(--color-gray-400);
|
|
790
|
+
background: var(--color-gray-50);
|
|
791
|
+
border-right: 1px solid var(--border-color-light);
|
|
792
|
+
transition: all 0.15s ease;
|
|
793
|
+
cursor: grab;
|
|
794
|
+
user-select: none;
|
|
795
|
+
touch-action: none; /* Prevents scroll on touch */
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
.drag-handle:active {
|
|
799
|
+
cursor: grabbing;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
.grid-row:hover .drag-handle {
|
|
803
|
+
color: var(--color-gray-600);
|
|
804
|
+
background: var(--color-gray-100);
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
.drag-handle i {
|
|
808
|
+
font-size: 1.1rem;
|
|
809
|
+
pointer-events: none;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
.grid-row.dragging {
|
|
813
|
+
opacity: 0.5;
|
|
814
|
+
background-color: var(--color-gray-200) !important;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
/* Long press animation for touch devices */
|
|
818
|
+
.grid-row.long-pressing {
|
|
819
|
+
animation: long-press-glow 0.5s ease-in-out forwards;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
@keyframes long-press-glow {
|
|
823
|
+
0% {
|
|
824
|
+
box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.6);
|
|
825
|
+
}
|
|
826
|
+
100% {
|
|
827
|
+
box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.4);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
.grid-row.drop-above {
|
|
832
|
+
position: relative;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
.grid-row.drop-above::before {
|
|
836
|
+
content: '';
|
|
837
|
+
position: absolute;
|
|
838
|
+
top: -2px;
|
|
839
|
+
left: 0;
|
|
840
|
+
right: 0;
|
|
841
|
+
height: 3px;
|
|
842
|
+
background-color: var(--color-primary);
|
|
843
|
+
border-radius: 2px;
|
|
844
|
+
z-index: 10;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
.grid-row.drop-below {
|
|
848
|
+
position: relative;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
.grid-row.drop-below::after {
|
|
852
|
+
content: '';
|
|
853
|
+
position: absolute;
|
|
854
|
+
bottom: -2px;
|
|
855
|
+
left: 0;
|
|
856
|
+
right: 0;
|
|
857
|
+
height: 3px;
|
|
858
|
+
background-color: var(--color-primary);
|
|
859
|
+
border-radius: 2px;
|
|
860
|
+
z-index: 10;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
/* Image styling */
|
|
864
|
+
.table-image {
|
|
865
|
+
width: 40px;
|
|
866
|
+
height: 40px;
|
|
867
|
+
object-fit: cover;
|
|
868
|
+
border-radius: var(--radius-md);
|
|
869
|
+
box-shadow: var(--shadow-xs);
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
.table-image-placeholder {
|
|
873
|
+
width: 40px;
|
|
874
|
+
height: 40px;
|
|
875
|
+
background-color: var(--color-gray-100);
|
|
876
|
+
border-radius: var(--radius-md);
|
|
877
|
+
display: flex;
|
|
878
|
+
align-items: center;
|
|
879
|
+
justify-content: center;
|
|
880
|
+
color: var(--color-gray-400);
|
|
881
|
+
font-size: var(--font-size-lg);
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
/* Empty state styling */
|
|
885
|
+
:global(.grid-view .card-body .text-muted) {
|
|
886
|
+
color: var(--color-gray-500);
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
:global(.grid-view .card-body h5) {
|
|
890
|
+
font-weight: var(--font-weight-semibold);
|
|
891
|
+
color: var(--color-gray-700);
|
|
892
|
+
font-size: var(--font-size-lg);
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
/* Search results counter */
|
|
896
|
+
:global(.grid-view small.text-muted) {
|
|
897
|
+
color: var(--color-gray-600);
|
|
898
|
+
font-size: var(--font-size-sm);
|
|
899
|
+
font-weight: var(--font-weight-medium);
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
/* Loading spinner */
|
|
903
|
+
:global(.grid-view .spinner-border) {
|
|
904
|
+
color: var(--color-primary);
|
|
905
|
+
width: var(--spacing-xl);
|
|
906
|
+
height: var(--spacing-xl);
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
/* Checkbox indeterminate state */
|
|
910
|
+
input[type="checkbox"]:indeterminate {
|
|
911
|
+
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e");
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
/* Dropdown styling */
|
|
915
|
+
.grid-cell .dropdown {
|
|
916
|
+
position: static;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
.grid-cell .dropdown-menu {
|
|
920
|
+
z-index: 1050;
|
|
921
|
+
position: fixed;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
/* Action button styling */
|
|
925
|
+
:global(.grid-view .btn-outline-secondary) {
|
|
926
|
+
border-color: var(--border-color-medium);
|
|
927
|
+
color: var(--color-gray-600);
|
|
928
|
+
transition: all var(--transition-fast);
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
:global(.grid-view .btn-outline-secondary:hover) {
|
|
932
|
+
background-color: var(--color-gray-50);
|
|
933
|
+
border-color: var(--border-color-dark);
|
|
934
|
+
color: var(--color-gray-800);
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
/* Dropdown menu items with variants */
|
|
938
|
+
.dropdown-item-success {
|
|
939
|
+
color: var(--color-success);
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
.dropdown-item-success:hover {
|
|
943
|
+
background-color: var(--color-success-light);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
.dropdown-item-danger {
|
|
947
|
+
color: var(--color-danger);
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
.dropdown-item-danger:hover {
|
|
951
|
+
background-color: var(--color-danger-light);
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
.dropdown-item-warning {
|
|
955
|
+
color: var(--color-warning);
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
.dropdown-item-warning:hover {
|
|
959
|
+
background-color: var(--color-warning-light);
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
.dropdown-item-info {
|
|
963
|
+
color: var(--color-info);
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
.dropdown-item-info:hover {
|
|
967
|
+
background-color: var(--color-info-light);
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
/* Pagination */
|
|
971
|
+
.grid-pagination {
|
|
972
|
+
display: flex;
|
|
973
|
+
flex-wrap: wrap;
|
|
974
|
+
align-items: center;
|
|
975
|
+
justify-content: center;
|
|
976
|
+
gap: var(--spacing-xs);
|
|
977
|
+
padding: var(--spacing-md) var(--spacing-lg);
|
|
978
|
+
border-top: 1px solid var(--color-border-subtle);
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
.page-btn {
|
|
982
|
+
min-width: 2.25rem;
|
|
983
|
+
height: 2.25rem;
|
|
984
|
+
padding: 0 var(--spacing-sm);
|
|
985
|
+
border: 1px solid var(--color-border-subtle);
|
|
986
|
+
border-radius: var(--radius-md);
|
|
987
|
+
background: var(--color-bg-surface);
|
|
988
|
+
color: var(--color-text-muted);
|
|
989
|
+
font-size: var(--font-size-sm);
|
|
990
|
+
font-weight: var(--font-weight-medium);
|
|
991
|
+
cursor: pointer;
|
|
992
|
+
transition: all var(--transition-fast);
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
.page-btn:hover:not(:disabled):not(.active) {
|
|
996
|
+
border-color: var(--color-primary);
|
|
997
|
+
color: var(--color-primary);
|
|
998
|
+
background: var(--color-primary-light);
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
.page-btn.active {
|
|
1002
|
+
background: var(--gradient-brand);
|
|
1003
|
+
border-color: transparent;
|
|
1004
|
+
color: #fff;
|
|
1005
|
+
box-shadow: var(--shadow-xs);
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
.page-btn:disabled {
|
|
1009
|
+
opacity: 0.45;
|
|
1010
|
+
cursor: not-allowed;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
.page-ellipsis {
|
|
1014
|
+
padding: 0 var(--spacing-xs);
|
|
1015
|
+
color: var(--color-text-muted);
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
/* Stacked avatars cell should not clip overflow */
|
|
1019
|
+
.grid-cell :global(.pet-avatars-stack) {
|
|
1020
|
+
overflow: visible;
|
|
1021
|
+
}
|
|
1022
|
+
</style>
|