@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.
Files changed (60) hide show
  1. package/README.md +125 -0
  2. package/dist/components/Alert.svelte +335 -0
  3. package/dist/components/Alert.svelte.d.ts +24 -0
  4. package/dist/components/AutocompleteInput.svelte +356 -0
  5. package/dist/components/AutocompleteInput.svelte.d.ts +72 -0
  6. package/dist/components/Badge.svelte +185 -0
  7. package/dist/components/Badge.svelte.d.ts +20 -0
  8. package/dist/components/Button.svelte +415 -0
  9. package/dist/components/Button.svelte.d.ts +34 -0
  10. package/dist/components/Card.svelte +181 -0
  11. package/dist/components/Card.svelte.d.ts +24 -0
  12. package/dist/components/CardBody.svelte +78 -0
  13. package/dist/components/CardBody.svelte.d.ts +12 -0
  14. package/dist/components/CardFooter.svelte +81 -0
  15. package/dist/components/CardFooter.svelte.d.ts +14 -0
  16. package/dist/components/CardHeader.svelte +186 -0
  17. package/dist/components/CardHeader.svelte.d.ts +21 -0
  18. package/dist/components/Col.svelte +172 -0
  19. package/dist/components/Col.svelte.d.ts +26 -0
  20. package/dist/components/Container.svelte +118 -0
  21. package/dist/components/Container.svelte.d.ts +14 -0
  22. package/dist/components/Drawer.svelte +233 -0
  23. package/dist/components/Drawer.svelte.d.ts +13 -0
  24. package/dist/components/Dropdown.svelte +190 -0
  25. package/dist/components/Dropdown.svelte.d.ts +26 -0
  26. package/dist/components/DropdownItem.svelte +103 -0
  27. package/dist/components/DropdownItem.svelte.d.ts +22 -0
  28. package/dist/components/DurationInput.svelte +170 -0
  29. package/dist/components/DurationInput.svelte.d.ts +27 -0
  30. package/dist/components/EditableTable.svelte +647 -0
  31. package/dist/components/EditableTable.svelte.d.ts +74 -0
  32. package/dist/components/EmptyState.svelte +192 -0
  33. package/dist/components/EmptyState.svelte.d.ts +22 -0
  34. package/dist/components/FormField.svelte +260 -0
  35. package/dist/components/FormField.svelte.d.ts +68 -0
  36. package/dist/components/GridView.svelte +1022 -0
  37. package/dist/components/GridView.svelte.d.ts +38 -0
  38. package/dist/components/GridView.types.d.ts +28 -0
  39. package/dist/components/GridView.types.js +1 -0
  40. package/dist/components/LoadingSpinner.svelte +253 -0
  41. package/dist/components/LoadingSpinner.svelte.d.ts +17 -0
  42. package/dist/components/Modal.svelte +473 -0
  43. package/dist/components/Modal.svelte.d.ts +42 -0
  44. package/dist/components/PhoneInput.svelte +406 -0
  45. package/dist/components/PhoneInput.svelte.d.ts +31 -0
  46. package/dist/components/PhotoUpload.svelte +529 -0
  47. package/dist/components/PhotoUpload.svelte.d.ts +46 -0
  48. package/dist/components/Row.svelte +153 -0
  49. package/dist/components/Row.svelte.d.ts +18 -0
  50. package/dist/icons/PawPrintIcon.svelte +41 -0
  51. package/dist/icons/PawPrintIcon.svelte.d.ts +14 -0
  52. package/dist/index.d.ts +41 -0
  53. package/dist/index.js +49 -0
  54. package/dist/styles/forms.css +182 -0
  55. package/dist/styles/tokens.css +243 -0
  56. package/dist/utils/duration.d.ts +20 -0
  57. package/dist/utils/duration.js +40 -0
  58. package/dist/utils/scrollLock.d.ts +7 -0
  59. package/dist/utils/scrollLock.js +26 -0
  60. package/package.json +66 -0
@@ -0,0 +1,647 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import Button from './Button.svelte';
4
+ import Badge from './Badge.svelte';
5
+ import DurationInput from './DurationInput.svelte';
6
+ import { formatDuration } from '../utils/duration.js';
7
+
8
+ /** Argumentos passados ao editor de célula custom (col.editCell). */
9
+ export interface EditCellArgs {
10
+ /** Linha em edição (editingData ou newRowData) */
11
+ data: Record<string, any>;
12
+ /** Atualiza um campo da linha em edição */
13
+ setField: (key: string, value: any) => void;
14
+ /** Edição em curso (desabilitar inputs) */
15
+ disabled: boolean;
16
+ /** Erros de validação por chave */
17
+ errors: Record<string, string>;
18
+ }
19
+
20
+ export interface EditableColumn {
21
+ /** Chave do campo no objeto */
22
+ key: string;
23
+ /** Título da coluna */
24
+ title: string;
25
+ /** Tipo de campo */
26
+ type: 'text' | 'number' | 'select' | 'badge' | 'duration' | 'custom';
27
+ /** Largura da coluna */
28
+ width?: string;
29
+ /** Campo editável */
30
+ editable?: boolean;
31
+ /** Campo obrigatório */
32
+ required?: boolean;
33
+ /** Opções para select */
34
+ options?: Array<{ value: any; label: string }>;
35
+ /** Valor mínimo para number */
36
+ min?: number;
37
+ /** Valor máximo para number */
38
+ max?: number;
39
+ /** Step para number */
40
+ step?: number;
41
+ /** Função para formatar exibição */
42
+ format?: (value: any) => string;
43
+ /** Exibição custom usando a linha inteira (type: 'custom') */
44
+ render?: (row: any) => string;
45
+ /** Editor custom usando a linha inteira (type: 'custom') */
46
+ editCell?: Snippet<[EditCellArgs]>;
47
+ /** Validação custom; devolve mensagem de erro ou null */
48
+ validate?: (data: Record<string, any>) => string | null;
49
+ /** Função para obter variante do badge */
50
+ badgeVariant?: (value: any) => 'primary' | 'secondary' | 'success' | 'danger' | 'warning' | 'info' | 'light' | 'dark';
51
+ /** Placeholder */
52
+ placeholder?: string;
53
+ }
54
+
55
+ interface EditableTableProps<T> {
56
+ /** Colunas da tabela */
57
+ columns: EditableColumn[];
58
+ /** Dados da tabela */
59
+ data: T[];
60
+ /** Callback ao guardar linha */
61
+ onSave: (row: T, isNew: boolean) => Promise<void>;
62
+ /** Callback ao apagar linha */
63
+ onDelete: (row: T) => Promise<void>;
64
+ /** Factory para nova linha */
65
+ createNewRow: () => Partial<T>;
66
+ /** Mensagem quando vazio */
67
+ emptyMessage?: string;
68
+ /** Chave única para identificar linhas */
69
+ rowKey?: string;
70
+ /** Mostrar loading global */
71
+ loading?: boolean;
72
+ /** Snippet para ações extras */
73
+ rowActions?: Snippet<[T]>;
74
+ /** Callback quando linha é marcada para exclusão */
75
+ onMarkDelete?: (row: T) => void;
76
+ }
77
+
78
+ let {
79
+ columns,
80
+ data = [],
81
+ onSave,
82
+ onDelete,
83
+ createNewRow,
84
+ emptyMessage = 'Nenhum registo encontrado',
85
+ rowKey = 'id',
86
+ loading = false,
87
+ rowActions,
88
+ onMarkDelete
89
+ }: EditableTableProps<any> = $props();
90
+
91
+ // Estado interno
92
+ let editingRowId: any = $state(null);
93
+ let editingData: Record<string, any> = $state({});
94
+ let isAddingNew: boolean = $state(false);
95
+ let newRowData: Record<string, any> = $state({});
96
+ let savingRowId: any = $state(null);
97
+ let deletingRowId: any = $state(null);
98
+ let errors: Record<string, string> = $state({});
99
+
100
+ // Iniciar edição de linha
101
+ function startEdit(row: any) {
102
+ editingRowId = row[rowKey];
103
+ editingData = { ...row };
104
+ errors = {};
105
+ }
106
+
107
+ // Cancelar edição
108
+ function cancelEdit() {
109
+ editingRowId = null;
110
+ editingData = {};
111
+ errors = {};
112
+ }
113
+
114
+ // Validar dados
115
+ function validateRow(rowData: Record<string, any>): boolean {
116
+ errors = {};
117
+ let isValid = true;
118
+
119
+ for (const col of columns) {
120
+ if (col.editable !== false && col.required) {
121
+ const value = rowData[col.key];
122
+ if (value === null || value === undefined || value === '') {
123
+ errors[col.key] = 'Campo obrigatório';
124
+ isValid = false;
125
+ }
126
+ }
127
+
128
+ if ((col.type === 'number' || col.type === 'duration') && rowData[col.key] !== null && rowData[col.key] !== undefined) {
129
+ const numValue = Number(rowData[col.key]);
130
+ if (col.min !== undefined && numValue < col.min) {
131
+ errors[col.key] = `Valor mínimo: ${col.min}`;
132
+ isValid = false;
133
+ }
134
+ if (col.max !== undefined && numValue > col.max) {
135
+ errors[col.key] = `Valor máximo: ${col.max}`;
136
+ isValid = false;
137
+ }
138
+ }
139
+
140
+ if (col.validate) {
141
+ const message = col.validate(rowData);
142
+ if (message) {
143
+ errors[col.key] = message;
144
+ isValid = false;
145
+ }
146
+ }
147
+ }
148
+
149
+ return isValid;
150
+ }
151
+
152
+ // Guardar edição
153
+ async function saveEdit() {
154
+ if (!validateRow(editingData)) return;
155
+
156
+ savingRowId = editingRowId;
157
+ try {
158
+ await onSave(editingData, false);
159
+ cancelEdit();
160
+ } catch (error) {
161
+ console.error('Erro ao guardar:', error);
162
+ } finally {
163
+ savingRowId = null;
164
+ }
165
+ }
166
+
167
+ // Iniciar adição de nova linha
168
+ function startAddNew() {
169
+ isAddingNew = true;
170
+ newRowData = createNewRow();
171
+ errors = {};
172
+ }
173
+
174
+ // Cancelar adição
175
+ function cancelAddNew() {
176
+ isAddingNew = false;
177
+ newRowData = {};
178
+ errors = {};
179
+ }
180
+
181
+ // Guardar nova linha
182
+ async function saveNewRow() {
183
+ if (!validateRow(newRowData)) return;
184
+
185
+ savingRowId = 'new';
186
+ try {
187
+ await onSave(newRowData, true);
188
+ cancelAddNew();
189
+ } catch (error) {
190
+ console.error('Erro ao criar:', error);
191
+ } finally {
192
+ savingRowId = null;
193
+ }
194
+ }
195
+
196
+ // Apagar linha
197
+ async function deleteRow(row: any) {
198
+ if (onMarkDelete) {
199
+ onMarkDelete(row);
200
+ return;
201
+ }
202
+
203
+ deletingRowId = row[rowKey];
204
+ try {
205
+ await onDelete(row);
206
+ } catch (error) {
207
+ console.error('Erro ao apagar:', error);
208
+ } finally {
209
+ deletingRowId = null;
210
+ }
211
+ }
212
+
213
+ // Resolve select value: match the string from DOM back to the original option value type
214
+ function resolveSelectValue(col: EditableColumn, rawValue: string): any {
215
+ if (!rawValue) return null;
216
+ const option = col.options?.find(o => String(o.value) === rawValue);
217
+ return option ? option.value : rawValue;
218
+ }
219
+
220
+ // Atualizar valor na edição
221
+ function updateEditValue(key: string, value: any) {
222
+ editingData[key] = value;
223
+ }
224
+
225
+ // Atualizar valor na nova linha
226
+ function updateNewValue(key: string, value: any) {
227
+ newRowData[key] = value;
228
+ }
229
+
230
+ // Formatar valor para exibição
231
+ function formatValue(col: EditableColumn, value: any): string {
232
+ if (value === null || value === undefined) return '-';
233
+
234
+ if (col.format) return col.format(value);
235
+
236
+ if (col.type === 'duration') return formatDuration(Number(value));
237
+
238
+ if (col.type === 'select' && col.options) {
239
+ const option = col.options.find(o => o.value === value);
240
+ return option?.label || String(value);
241
+ }
242
+
243
+ return String(value);
244
+ }
245
+
246
+ // Obter variante do badge
247
+ function getBadgeVariant(col: EditableColumn, value: any): 'primary' | 'secondary' | 'success' | 'danger' | 'warning' | 'info' | 'light' | 'dark' {
248
+ if (col.badgeVariant) return col.badgeVariant(value);
249
+ return 'secondary';
250
+ }
251
+ </script>
252
+
253
+ <!-- Editor de uma célula, partilhado entre a linha de edição e a de adição -->
254
+ {#snippet cellEditor(col: EditableColumn, rowData: Record<string, any>, setField: (key: string, value: any) => void, disabled: boolean, idScope: string)}
255
+ {#if col.type === 'custom' && col.editCell}
256
+ {@render col.editCell({ data: rowData, setField, disabled, errors })}
257
+ {:else if col.type === 'select'}
258
+ <select
259
+ class="form-select form-select-sm"
260
+ value={rowData[col.key] ?? ''}
261
+ onchange={(e) => setField(col.key, resolveSelectValue(col, e.currentTarget.value))}
262
+ {disabled}
263
+ >
264
+ <option value="">{col.placeholder || 'Selecione...'}</option>
265
+ {#each col.options || [] as option}
266
+ <option value={option.value}>{option.label}</option>
267
+ {/each}
268
+ </select>
269
+ {:else if col.type === 'number'}
270
+ <input
271
+ type="number"
272
+ class="form-control form-control-sm"
273
+ value={rowData[col.key] ?? ''}
274
+ oninput={(e) => setField(col.key, e.currentTarget.value ? Number(e.currentTarget.value) : null)}
275
+ min={col.min}
276
+ max={col.max}
277
+ step={col.step || 1}
278
+ placeholder={col.placeholder}
279
+ {disabled}
280
+ />
281
+ {:else if col.type === 'duration'}
282
+ <DurationInput
283
+ id={`dur-${idScope}-${col.key}`}
284
+ size="sm"
285
+ value={rowData[col.key] ?? null}
286
+ onchange={(m) => setField(col.key, m)}
287
+ {disabled}
288
+ />
289
+ {:else}
290
+ <input
291
+ type="text"
292
+ class="form-control form-control-sm"
293
+ value={rowData[col.key] ?? ''}
294
+ oninput={(e) => setField(col.key, e.currentTarget.value)}
295
+ placeholder={col.placeholder}
296
+ {disabled}
297
+ />
298
+ {/if}
299
+ {/snippet}
300
+
301
+ <div class="editable-table-wrapper">
302
+ {#if loading}
303
+ <div class="loading-overlay">
304
+ <div class="spinner-border text-primary" role="status">
305
+ <span class="visually-hidden">A carregar...</span>
306
+ </div>
307
+ </div>
308
+ {/if}
309
+
310
+ <div class="table-responsive">
311
+ <table class="table editable-table">
312
+ <thead>
313
+ <tr>
314
+ {#each columns as col}
315
+ <th style={col.width ? `width: ${col.width}` : ''}>
316
+ {col.title}
317
+ {#if col.required}
318
+ <span class="text-danger">*</span>
319
+ {/if}
320
+ </th>
321
+ {/each}
322
+ <th class="actions-column">Ações</th>
323
+ </tr>
324
+ </thead>
325
+ <tbody>
326
+ {#if data.length === 0 && !isAddingNew}
327
+ <tr class="empty-row">
328
+ <td colspan={columns.length + 1}>
329
+ <div class="empty-message">
330
+ <i class="bi bi-inbox"></i>
331
+ <span>{emptyMessage}</span>
332
+ </div>
333
+ </td>
334
+ </tr>
335
+ {/if}
336
+
337
+ {#each data as row (row[rowKey])}
338
+ {@const isEditing = editingRowId === row[rowKey]}
339
+ {@const isSaving = savingRowId === row[rowKey]}
340
+ {@const isDeleting = deletingRowId === row[rowKey]}
341
+
342
+ <tr class:editing={isEditing} class:saving={isSaving} class:deleting={isDeleting}>
343
+ {#each columns as col}
344
+ <td data-label={col.title} class:has-error={isEditing && errors[col.key]}>
345
+ {#if isEditing && col.editable !== false}
346
+ {@render cellEditor(col, editingData, updateEditValue, isSaving, String(editingRowId))}
347
+ {#if errors[col.key]}
348
+ <div class="field-error">{errors[col.key]}</div>
349
+ {/if}
350
+ {:else if col.type === 'custom'}
351
+ <span class="cell-value">{col.render ? col.render(row) : formatValue(col, row[col.key])}</span>
352
+ {:else if col.type === 'badge'}
353
+ <Badge variant={getBadgeVariant(col, row[col.key])} size="sm">
354
+ {formatValue(col, row[col.key])}
355
+ </Badge>
356
+ {:else}
357
+ <span class="cell-value">{formatValue(col, row[col.key])}</span>
358
+ {/if}
359
+ </td>
360
+ {/each}
361
+ <td class="actions-cell" data-label="Ações">
362
+ {#if isEditing}
363
+ <div class="action-buttons">
364
+ <Button
365
+ variant="success"
366
+ size="sm"
367
+ onclick={saveEdit}
368
+ loading={isSaving}
369
+ disabled={isSaving}
370
+ >
371
+ <i class="bi bi-check-lg"></i>
372
+ </Button>
373
+ <Button
374
+ variant="outline-secondary"
375
+ size="sm"
376
+ onclick={cancelEdit}
377
+ disabled={isSaving}
378
+ >
379
+ <i class="bi bi-x-lg"></i>
380
+ </Button>
381
+ </div>
382
+ {:else}
383
+ <div class="action-buttons">
384
+ <Button
385
+ variant="outline-primary"
386
+ size="sm"
387
+ onclick={() => startEdit(row)}
388
+ disabled={editingRowId !== null || isAddingNew || isDeleting}
389
+ >
390
+ <i class="bi bi-pencil"></i>
391
+ </Button>
392
+ {#if rowActions}
393
+ {@render rowActions(row)}
394
+ {/if}
395
+ <Button
396
+ variant="outline-danger"
397
+ size="sm"
398
+ onclick={() => deleteRow(row)}
399
+ loading={isDeleting}
400
+ disabled={editingRowId !== null || isAddingNew || isDeleting}
401
+ >
402
+ <i class="bi bi-trash"></i>
403
+ </Button>
404
+ </div>
405
+ {/if}
406
+ </td>
407
+ </tr>
408
+ {/each}
409
+
410
+ <!-- Linha para adicionar novo -->
411
+ {#if isAddingNew}
412
+ {@const isSaving = savingRowId === 'new'}
413
+ <tr class="new-row" class:saving={isSaving}>
414
+ {#each columns as col}
415
+ <td data-label={col.title} class:has-error={errors[col.key]}>
416
+ {#if col.editable !== false}
417
+ {@render cellEditor(col, newRowData, updateNewValue, isSaving, 'new')}
418
+ {#if errors[col.key]}
419
+ <div class="field-error">{errors[col.key]}</div>
420
+ {/if}
421
+ {:else}
422
+ <span class="cell-readonly">-</span>
423
+ {/if}
424
+ </td>
425
+ {/each}
426
+ <td class="actions-cell" data-label="Ações">
427
+ <div class="action-buttons">
428
+ <Button
429
+ variant="success"
430
+ size="sm"
431
+ onclick={saveNewRow}
432
+ loading={isSaving}
433
+ disabled={isSaving}
434
+ >
435
+ <i class="bi bi-check-lg"></i>
436
+ </Button>
437
+ <Button
438
+ variant="outline-secondary"
439
+ size="sm"
440
+ onclick={cancelAddNew}
441
+ disabled={isSaving}
442
+ >
443
+ <i class="bi bi-x-lg"></i>
444
+ </Button>
445
+ </div>
446
+ </td>
447
+ </tr>
448
+ {/if}
449
+ </tbody>
450
+ </table>
451
+ </div>
452
+
453
+ <!-- Botão para adicionar nova linha -->
454
+ {#if !isAddingNew && editingRowId === null}
455
+ <div class="add-row-container">
456
+ <Button
457
+ variant="outline-primary"
458
+ size="sm"
459
+ onclick={startAddNew}
460
+ disabled={loading}
461
+ >
462
+ <i class="bi bi-plus-lg me-1"></i>
463
+ Adicionar
464
+ </Button>
465
+ </div>
466
+ {/if}
467
+ </div>
468
+
469
+ <style>
470
+ .editable-table-wrapper {
471
+ position: relative;
472
+ }
473
+
474
+ .loading-overlay {
475
+ position: absolute;
476
+ inset: 0;
477
+ background: rgba(255, 255, 255, 0.8);
478
+ display: flex;
479
+ align-items: center;
480
+ justify-content: center;
481
+ z-index: 10;
482
+ border-radius: var(--radius-md);
483
+ }
484
+
485
+ .table-responsive {
486
+ overflow-x: auto;
487
+ }
488
+
489
+ .editable-table {
490
+ width: 100%;
491
+ margin-bottom: 0;
492
+ font-size: var(--font-size-sm);
493
+ }
494
+
495
+ .editable-table thead th {
496
+ background-color: var(--color-gray-50);
497
+ border-bottom: 2px solid var(--color-gray-200);
498
+ padding: var(--spacing-sm) var(--spacing-md);
499
+ font-weight: var(--font-weight-semibold);
500
+ color: var(--color-gray-700);
501
+ white-space: nowrap;
502
+ }
503
+
504
+ .editable-table tbody td {
505
+ padding: var(--spacing-sm) var(--spacing-md);
506
+ vertical-align: middle;
507
+ border-bottom: 1px solid var(--color-gray-100);
508
+ }
509
+
510
+ .editable-table tbody tr:hover {
511
+ background-color: var(--color-gray-50);
512
+ }
513
+
514
+ .editable-table tbody tr.editing {
515
+ background-color: var(--color-primary-light, #e7f3ff);
516
+ }
517
+
518
+ .editable-table tbody tr.new-row {
519
+ background-color: var(--color-success-light, #e8f5e9);
520
+ }
521
+
522
+ .editable-table tbody tr.saving {
523
+ opacity: 0.7;
524
+ pointer-events: none;
525
+ }
526
+
527
+ .editable-table tbody tr.deleting {
528
+ opacity: 0.5;
529
+ background-color: var(--color-danger-light, #ffebee);
530
+ }
531
+
532
+ .actions-column {
533
+ width: 120px;
534
+ text-align: center;
535
+ }
536
+
537
+ .actions-cell {
538
+ text-align: center;
539
+ }
540
+
541
+ .action-buttons {
542
+ display: flex;
543
+ gap: var(--spacing-xs);
544
+ justify-content: center;
545
+ }
546
+
547
+ .cell-value {
548
+ color: var(--color-gray-800);
549
+ }
550
+
551
+ .cell-readonly {
552
+ color: var(--color-gray-400);
553
+ font-style: italic;
554
+ }
555
+
556
+ .empty-row td {
557
+ padding: var(--spacing-xl) !important;
558
+ }
559
+
560
+ .empty-message {
561
+ display: flex;
562
+ flex-direction: column;
563
+ align-items: center;
564
+ gap: var(--spacing-sm);
565
+ color: var(--color-gray-500);
566
+ }
567
+
568
+ .empty-message i {
569
+ font-size: 2rem;
570
+ }
571
+
572
+ .add-row-container {
573
+ padding: var(--spacing-md);
574
+ border-top: 1px dashed var(--color-gray-200);
575
+ text-align: center;
576
+ }
577
+
578
+ .has-error {
579
+ position: relative;
580
+ }
581
+
582
+ .has-error .form-control,
583
+ .has-error .form-select {
584
+ border-color: var(--color-danger);
585
+ }
586
+
587
+ .field-error {
588
+ position: absolute;
589
+ bottom: -18px;
590
+ left: 0;
591
+ font-size: var(--font-size-xs);
592
+ color: var(--color-danger);
593
+ white-space: nowrap;
594
+ }
595
+
596
+ /* Inputs dentro da tabela */
597
+ .editable-table .form-control,
598
+ .editable-table .form-select {
599
+ min-width: 80px;
600
+ }
601
+
602
+ .editable-table .form-control:focus,
603
+ .editable-table .form-select:focus {
604
+ box-shadow: none;
605
+ border-color: var(--color-primary);
606
+ }
607
+
608
+ /* Responsivo */
609
+ @media (max-width: 768px) {
610
+ .editable-table thead {
611
+ display: none;
612
+ }
613
+
614
+ .editable-table tbody tr {
615
+ display: block;
616
+ margin-bottom: var(--spacing-md);
617
+ border: 1px solid var(--color-gray-200);
618
+ border-radius: var(--radius-md);
619
+ }
620
+
621
+ .editable-table tbody td {
622
+ display: flex;
623
+ justify-content: flex-start;
624
+ align-items: center;
625
+ padding: var(--spacing-sm);
626
+ border-bottom: 1px solid var(--color-gray-100);
627
+ gap: var(--spacing-sm);
628
+ }
629
+
630
+ .editable-table tbody td::before {
631
+ content: attr(data-label);
632
+ font-weight: var(--font-weight-semibold);
633
+ color: var(--color-gray-600);
634
+ flex-shrink: 0;
635
+ min-width: 80px;
636
+ }
637
+
638
+ .actions-cell {
639
+ justify-content: flex-end !important;
640
+ }
641
+
642
+ .add-row-container {
643
+ border-top: none;
644
+ margin-top: var(--spacing-md);
645
+ }
646
+ }
647
+ </style>
@@ -0,0 +1,74 @@
1
+ import type { Snippet } from 'svelte';
2
+ /** Argumentos passados ao editor de célula custom (col.editCell). */
3
+ export interface EditCellArgs {
4
+ /** Linha em edição (editingData ou newRowData) */
5
+ data: Record<string, any>;
6
+ /** Atualiza um campo da linha em edição */
7
+ setField: (key: string, value: any) => void;
8
+ /** Edição em curso (desabilitar inputs) */
9
+ disabled: boolean;
10
+ /** Erros de validação por chave */
11
+ errors: Record<string, string>;
12
+ }
13
+ export interface EditableColumn {
14
+ /** Chave do campo no objeto */
15
+ key: string;
16
+ /** Título da coluna */
17
+ title: string;
18
+ /** Tipo de campo */
19
+ type: 'text' | 'number' | 'select' | 'badge' | 'duration' | 'custom';
20
+ /** Largura da coluna */
21
+ width?: string;
22
+ /** Campo editável */
23
+ editable?: boolean;
24
+ /** Campo obrigatório */
25
+ required?: boolean;
26
+ /** Opções para select */
27
+ options?: Array<{
28
+ value: any;
29
+ label: string;
30
+ }>;
31
+ /** Valor mínimo para number */
32
+ min?: number;
33
+ /** Valor máximo para number */
34
+ max?: number;
35
+ /** Step para number */
36
+ step?: number;
37
+ /** Função para formatar exibição */
38
+ format?: (value: any) => string;
39
+ /** Exibição custom usando a linha inteira (type: 'custom') */
40
+ render?: (row: any) => string;
41
+ /** Editor custom usando a linha inteira (type: 'custom') */
42
+ editCell?: Snippet<[EditCellArgs]>;
43
+ /** Validação custom; devolve mensagem de erro ou null */
44
+ validate?: (data: Record<string, any>) => string | null;
45
+ /** Função para obter variante do badge */
46
+ badgeVariant?: (value: any) => 'primary' | 'secondary' | 'success' | 'danger' | 'warning' | 'info' | 'light' | 'dark';
47
+ /** Placeholder */
48
+ placeholder?: string;
49
+ }
50
+ interface EditableTableProps<T> {
51
+ /** Colunas da tabela */
52
+ columns: EditableColumn[];
53
+ /** Dados da tabela */
54
+ data: T[];
55
+ /** Callback ao guardar linha */
56
+ onSave: (row: T, isNew: boolean) => Promise<void>;
57
+ /** Callback ao apagar linha */
58
+ onDelete: (row: T) => Promise<void>;
59
+ /** Factory para nova linha */
60
+ createNewRow: () => Partial<T>;
61
+ /** Mensagem quando vazio */
62
+ emptyMessage?: string;
63
+ /** Chave única para identificar linhas */
64
+ rowKey?: string;
65
+ /** Mostrar loading global */
66
+ loading?: boolean;
67
+ /** Snippet para ações extras */
68
+ rowActions?: Snippet<[T]>;
69
+ /** Callback quando linha é marcada para exclusão */
70
+ onMarkDelete?: (row: T) => void;
71
+ }
72
+ declare const EditableTable: import("svelte").Component<EditableTableProps<any>, {}, "">;
73
+ type EditableTable = ReturnType<typeof EditableTable>;
74
+ export default EditableTable;