@sonny-ui/core 0.1.0-alpha.12 → 0.1.0-alpha.14

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.
@@ -0,0 +1,603 @@
1
+ import {
2
+ ChangeDetectionStrategy,
3
+ Component,
4
+ TemplateRef,
5
+ computed,
6
+ contentChild,
7
+ contentChildren,
8
+ effect,
9
+ input,
10
+ model,
11
+ output,
12
+ signal,
13
+ untracked,
14
+ } from '@angular/core';
15
+ import { NgTemplateOutlet } from '@angular/common';
16
+ import {
17
+ SnyTableDirective,
18
+ SnyTableHeaderDirective,
19
+ SnyTableBodyDirective,
20
+ SnyTableRowDirective,
21
+ SnyTableHeadDirective,
22
+ SnyTableCellDirective,
23
+ } from '../table/table.directives';
24
+ import type { TableVariant, TableDensity } from '../table/table.variants';
25
+ import { SnyPaginationComponent } from '../pagination/pagination.component';
26
+ import { SnyCheckboxDirective } from '../checkbox/checkbox.directive';
27
+ import { SnyInputDirective } from '../input/input.directive';
28
+ import { SnyButtonDirective } from '../button/button.directive';
29
+ import { SnySelectComponent, type SelectOption } from '../select/select.component';
30
+ import { SnySkeletonDirective } from '../skeleton/skeleton.directive';
31
+ import {
32
+ SnyDropdownDirective,
33
+ SnyDropdownTriggerDirective,
34
+ SnyDropdownContentDirective,
35
+ SnyMenuItemDirective,
36
+ } from '../dropdown/dropdown.directives';
37
+ import {
38
+ SnyCellDefDirective,
39
+ SnyHeaderCellDefDirective,
40
+ SnyBulkActionsDefDirective,
41
+ SnyRowExpandDefDirective,
42
+ } from './data-table.directives';
43
+ import type {
44
+ DataTableColumn,
45
+ DataTablePaginationConfig,
46
+ SortState,
47
+ SortDirection,
48
+ } from './data-table.types';
49
+
50
+ const DEFAULT_PAGINATION: DataTablePaginationConfig = {
51
+ pageSize: 10,
52
+ pageSizeOptions: [5, 10, 25, 50],
53
+ };
54
+
55
+ @Component({
56
+ selector: 'sny-data-table',
57
+ standalone: true,
58
+ changeDetection: ChangeDetectionStrategy.OnPush,
59
+ imports: [
60
+ NgTemplateOutlet,
61
+ SnyTableDirective,
62
+ SnyTableHeaderDirective,
63
+ SnyTableBodyDirective,
64
+ SnyTableRowDirective,
65
+ SnyTableHeadDirective,
66
+ SnyTableCellDirective,
67
+ SnyPaginationComponent,
68
+ SnyCheckboxDirective,
69
+ SnyInputDirective,
70
+ SnyButtonDirective,
71
+ SnySelectComponent,
72
+ SnySkeletonDirective,
73
+ SnyDropdownDirective,
74
+ SnyDropdownTriggerDirective,
75
+ SnyDropdownContentDirective,
76
+ SnyMenuItemDirective,
77
+ ],
78
+ template: `
79
+ <!-- Toolbar -->
80
+ @if (filterable() || showExport() || showColumnToggle()) {
81
+ <div class="flex items-center justify-between gap-4 mb-4 flex-wrap">
82
+ @if (filterable()) {
83
+ <input
84
+ snyInput
85
+ [value]="filterText()"
86
+ (input)="onFilterInput($event)"
87
+ placeholder="Filter..."
88
+ class="w-full sm:max-w-sm"
89
+ />
90
+ }
91
+ <div class="flex items-center gap-2">
92
+ @if (showExport()) {
93
+ <button snyBtn variant="outline" size="sm" (click)="onExport()">
94
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="sm:mr-2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8Z"/><path d="M14 2v6h6"/><path d="M12 18v-6"/><path d="m9 15 3-3 3 3"/></svg>
95
+ <span class="hidden sm:inline">Export</span>
96
+ </button>
97
+ }
98
+ @if (showColumnToggle()) {
99
+ <div snyDropdown class="relative">
100
+ <button snyBtn variant="outline" size="sm" snyDropdownTrigger>
101
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="sm:mr-2"><path d="M12 3v18"/><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M3 9h18"/><path d="M3 15h18"/></svg>
102
+ <span class="hidden sm:inline">Columns</span>
103
+ </button>
104
+ <div snyDropdownContent class="w-48 right-0 left-auto">
105
+ @for (col of columns(); track col.key) {
106
+ <label snyMenuItem class="flex items-center gap-2 cursor-pointer">
107
+ <input
108
+ type="checkbox"
109
+ snyCheckbox
110
+ [checked]="!hiddenColumns().has(col.key)"
111
+ (change)="toggleColumnVisibility(col.key)"
112
+ (click)="$event.stopPropagation()"
113
+ />
114
+ {{ col.label }}
115
+ </label>
116
+ }
117
+ </div>
118
+ </div>
119
+ }
120
+ </div>
121
+ </div>
122
+ }
123
+
124
+ <!-- Bulk Actions Bar -->
125
+ @if (showBulkActions()) {
126
+ @let selected = selectedRows();
127
+ <div class="flex items-center gap-2 mb-4 p-3 bg-muted/50 rounded-sm border border-border flex-wrap">
128
+ <span class="text-sm font-medium text-muted-foreground mr-2">
129
+ {{ selected.length }} selected
130
+ </span>
131
+ <ng-container
132
+ [ngTemplateOutlet]="bulkActionsDef()!.template"
133
+ [ngTemplateOutletContext]="{ $implicit: selected }"
134
+ />
135
+ <button
136
+ snyBtn variant="ghost" size="sm" class="ml-auto"
137
+ (click)="selectedRows.set([])"
138
+ >
139
+ Clear
140
+ </button>
141
+ </div>
142
+ }
143
+
144
+ <!-- Table -->
145
+ <div class="overflow-auto border border-border rounded-sm">
146
+ <table
147
+ snyTable
148
+ [variant]="variant()"
149
+ [density]="density()"
150
+ [hoverable]="hoverable()"
151
+ [stickyHeader]="stickyHeader()"
152
+ >
153
+ <thead snyTableHeader>
154
+ <tr snyTableRow>
155
+ @if (selectable()) {
156
+ <th snyTableHead class="w-12">
157
+ <input
158
+ type="checkbox"
159
+ snyCheckbox
160
+ [checked]="allSelected()"
161
+ [indeterminate]="someSelected() && !allSelected()"
162
+ (change)="toggleSelectAll()"
163
+ />
164
+ </th>
165
+ }
166
+ @if (expandable()) {
167
+ <th snyTableHead class="w-10"></th>
168
+ }
169
+ @let sort = sortState();
170
+ @let headerDefs = headerCellDefMap();
171
+ @for (col of visibleColumns(); track col.key) {
172
+ <th
173
+ snyTableHead
174
+ [style.width]="col.width ?? null"
175
+ [class]="col.sortable ? 'cursor-pointer select-none' : ''"
176
+ (click)="col.sortable ? toggleSort(col.key) : null"
177
+ >
178
+ @if (headerDefs.has(col.key)) {
179
+ <ng-container
180
+ [ngTemplateOutlet]="headerDefs.get(col.key)!"
181
+ [ngTemplateOutletContext]="{ $implicit: col }"
182
+ />
183
+ } @else {
184
+ <div class="flex items-center gap-1">
185
+ <span>{{ col.label }}</span>
186
+ @if (col.sortable) {
187
+ @let isActive = sort.key === col.key;
188
+ @if (isActive && sort.direction === 'asc') {
189
+ <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m5 12 7-7 7 7"/></svg>
190
+ } @else if (isActive && sort.direction === 'desc') {
191
+ <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m19 12-7 7-7-7"/></svg>
192
+ } @else {
193
+ <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="opacity-30"><path d="m7 15 5 5 5-5"/><path d="m7 9 5-5 5 5"/></svg>
194
+ }
195
+ }
196
+ </div>
197
+ }
198
+ </th>
199
+ }
200
+ </tr>
201
+ </thead>
202
+ <tbody snyTableBody>
203
+ @if (loading()) {
204
+ @for (i of skeletonRows(); track i) {
205
+ <tr snyTableRow>
206
+ @if (selectable()) {
207
+ <td snyTableCell class="w-12"><div snySkeleton class="w-4 h-4 rounded"></div></td>
208
+ }
209
+ @if (expandable()) {
210
+ <td snyTableCell class="w-10"><div snySkeleton class="w-4 h-4 rounded"></div></td>
211
+ }
212
+ @for (col of visibleColumns(); track col.key) {
213
+ <td snyTableCell [style.width]="col.width ?? null">
214
+ <div snySkeleton class="w-full h-4 rounded"></div>
215
+ </td>
216
+ }
217
+ </tr>
218
+ }
219
+ } @else if (paginatedData().length === 0) {
220
+ <tr snyTableRow>
221
+ <td
222
+ snyTableCell
223
+ [attr.colspan]="totalColSpan()"
224
+ class="text-center text-muted-foreground py-8"
225
+ >
226
+ {{ noDataText() }}
227
+ </td>
228
+ </tr>
229
+ } @else {
230
+ @let cellDefs = cellDefMap();
231
+ @let cols = visibleColumns();
232
+ @let expandTpl = rowExpandDef();
233
+ @for (row of paginatedData(); track trackByFn(row, $index)) {
234
+ <tr
235
+ snyTableRow
236
+ [attr.data-state]="isSelected(row) ? 'selected' : null"
237
+ (click)="onRowClick(row)"
238
+ class="cursor-pointer"
239
+ >
240
+ @if (selectable()) {
241
+ <td snyTableCell class="w-12">
242
+ <input
243
+ type="checkbox"
244
+ snyCheckbox
245
+ [checked]="isSelected(row)"
246
+ (change)="toggleRowSelection(row)"
247
+ (click)="$event.stopPropagation()"
248
+ />
249
+ </td>
250
+ }
251
+ @if (expandable()) {
252
+ <td snyTableCell class="w-10">
253
+ <button
254
+ class="p-0.5 rounded hover:bg-accent transition-transform duration-150"
255
+ [class.rotate-90]="isExpanded(row)"
256
+ (click)="toggleRowExpansion(row); $event.stopPropagation()"
257
+ >
258
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6"/></svg>
259
+ </button>
260
+ </td>
261
+ }
262
+ @for (col of cols; track col.key) {
263
+ <td snyTableCell [style.width]="col.width ?? null">
264
+ @if (cellDefs.has(col.key)) {
265
+ <ng-container
266
+ [ngTemplateOutlet]="cellDefs.get(col.key)!"
267
+ [ngTemplateOutletContext]="{ $implicit: row[col.key], row: row }"
268
+ />
269
+ } @else {
270
+ {{ row[col.key] }}
271
+ }
272
+ </td>
273
+ }
274
+ </tr>
275
+ @if (expandable() && isExpanded(row) && expandTpl) {
276
+ <tr snyTableRow>
277
+ <td snyTableCell [attr.colspan]="totalColSpan()" class="bg-muted/30">
278
+ <ng-container
279
+ [ngTemplateOutlet]="expandTpl.template"
280
+ [ngTemplateOutletContext]="{ $implicit: row }"
281
+ />
282
+ </td>
283
+ </tr>
284
+ }
285
+ }
286
+ }
287
+ </tbody>
288
+ </table>
289
+ </div>
290
+
291
+ <!-- Footer -->
292
+ @if (paginated()) {
293
+ <div class="flex flex-col sm:flex-row items-start sm:items-center justify-between mt-4 gap-3 sm:gap-4">
294
+ <span class="text-sm text-muted-foreground">
295
+ @if (selectable()) {
296
+ {{ selectedRows().length }} of {{ filteredData().length }} row(s) selected
297
+ } @else {
298
+ {{ filteredData().length }} row(s)
299
+ }
300
+ </span>
301
+ <div class="flex items-center gap-3 sm:gap-4 flex-wrap">
302
+ <div class="flex items-center gap-2">
303
+ <span class="hidden sm:inline text-sm text-muted-foreground whitespace-nowrap">Rows per page</span>
304
+ <sny-select
305
+ [options]="pageSizeOptions()"
306
+ [value]="pageSizeValue()"
307
+ (valueChange)="onPageSizeChange($event)"
308
+ size="sm"
309
+ class="w-20"
310
+ />
311
+ </div>
312
+ <sny-pagination
313
+ [currentPage]="currentPage()"
314
+ (currentPageChange)="currentPage.set($event)"
315
+ [totalPages]="totalPages()"
316
+ />
317
+ </div>
318
+ </div>
319
+ }
320
+ `,
321
+ })
322
+ export class SnyDataTableComponent {
323
+ // Inputs
324
+ readonly columns = input.required<DataTableColumn[]>();
325
+ readonly data = input.required<Record<string, unknown>[]>();
326
+ readonly variant = input<TableVariant>('default');
327
+ readonly density = input<TableDensity>('normal');
328
+ readonly hoverable = input(true);
329
+ readonly stickyHeader = input(false);
330
+ readonly selectable = input(false);
331
+ readonly paginated = input(true);
332
+ readonly filterable = input(true);
333
+ readonly showExport = input(false);
334
+ readonly showColumnToggle = input(false);
335
+ readonly expandable = input(false);
336
+ readonly loading = input(false);
337
+ readonly loadingRows = input(5);
338
+ readonly paginationConfig = input<DataTablePaginationConfig>(DEFAULT_PAGINATION);
339
+ readonly trackBy = input('');
340
+ readonly noDataText = input('No data available');
341
+
342
+ // Model
343
+ readonly selectedRows = model<Record<string, unknown>[]>([]);
344
+
345
+ // Outputs
346
+ readonly sortChanged = output<SortState>();
347
+ readonly rowClicked = output<Record<string, unknown>>();
348
+ readonly dataExported = output<Record<string, unknown>[]>();
349
+
350
+ // Content queries
351
+ readonly cellDefs = contentChildren(SnyCellDefDirective);
352
+ readonly headerCellDefs = contentChildren(SnyHeaderCellDefDirective);
353
+ readonly bulkActionsDef = contentChild(SnyBulkActionsDefDirective);
354
+ readonly rowExpandDef = contentChild(SnyRowExpandDefDirective);
355
+
356
+ // Internal state
357
+ readonly sortState = signal<SortState>({ key: '', direction: null });
358
+ readonly filterText = signal('');
359
+ readonly currentPage = signal(1);
360
+ readonly pageSize = signal(10);
361
+ readonly hiddenColumns = signal<Set<string>>(new Set());
362
+ readonly expandedRows = signal<Set<unknown>>(new Set());
363
+
364
+ // Template def maps
365
+ readonly cellDefMap = computed(() => {
366
+ const map = new Map<string, TemplateRef<unknown>>();
367
+ for (const def of this.cellDefs()) {
368
+ map.set(def.snyCell(), def.template);
369
+ }
370
+ return map;
371
+ });
372
+
373
+ readonly headerCellDefMap = computed(() => {
374
+ const map = new Map<string, TemplateRef<unknown>>();
375
+ for (const def of this.headerCellDefs()) {
376
+ map.set(def.snyHeaderCell(), def.template);
377
+ }
378
+ return map;
379
+ });
380
+
381
+ // Visible columns
382
+ readonly visibleColumns = computed(() =>
383
+ this.columns().filter(
384
+ (col) => col.visible !== false && !this.hiddenColumns().has(col.key)
385
+ )
386
+ );
387
+
388
+ // Page size options
389
+ readonly pageSizeOptions = computed<SelectOption[]>(() =>
390
+ this.paginationConfig().pageSizeOptions.map((n) => ({
391
+ value: String(n),
392
+ label: String(n),
393
+ }))
394
+ );
395
+
396
+ readonly pageSizeValue = computed(() => String(this.pageSize()));
397
+
398
+ // Skeleton rows
399
+ readonly skeletonRows = computed(() =>
400
+ Array.from({ length: this.loadingRows() }, (_, i) => i)
401
+ );
402
+
403
+ // Bulk actions visibility
404
+ readonly showBulkActions = computed(
405
+ () =>
406
+ this.selectable() &&
407
+ this.selectedRows().length > 0 &&
408
+ this.bulkActionsDef() != null
409
+ );
410
+
411
+ // Data pipeline (filter uses all columns, not just visible)
412
+ readonly filteredData = computed(() => {
413
+ const text = this.filterText().toLowerCase().trim();
414
+ const rows = this.data();
415
+ if (!text) return rows;
416
+ const cols = this.columns().filter((c) => c.filterable !== false);
417
+ return rows.filter((row) =>
418
+ cols.some((col) =>
419
+ String(row[col.key] ?? '').toLowerCase().includes(text)
420
+ )
421
+ );
422
+ });
423
+
424
+ readonly sortedData = computed(() => {
425
+ const { key, direction } = this.sortState();
426
+ const rows = this.filteredData();
427
+ if (!key || !direction) return rows;
428
+ return [...rows].sort((a, b) => {
429
+ const aVal = a[key];
430
+ const bVal = b[key];
431
+ if (aVal == null && bVal == null) return 0;
432
+ if (aVal == null) return direction === 'asc' ? -1 : 1;
433
+ if (bVal == null) return direction === 'asc' ? 1 : -1;
434
+ if (typeof aVal === 'number' && typeof bVal === 'number') {
435
+ return direction === 'asc' ? aVal - bVal : bVal - aVal;
436
+ }
437
+ const cmp = String(aVal).localeCompare(String(bVal));
438
+ return direction === 'asc' ? cmp : -cmp;
439
+ });
440
+ });
441
+
442
+ readonly totalPages = computed(() =>
443
+ Math.max(1, Math.ceil(this.filteredData().length / this.pageSize()))
444
+ );
445
+
446
+ readonly paginatedData = computed(() => {
447
+ if (!this.paginated()) return this.sortedData();
448
+ const start = (this.currentPage() - 1) * this.pageSize();
449
+ return this.sortedData().slice(start, start + this.pageSize());
450
+ });
451
+
452
+ readonly totalColSpan = computed(
453
+ () =>
454
+ this.visibleColumns().length +
455
+ (this.selectable() ? 1 : 0) +
456
+ (this.expandable() ? 1 : 0)
457
+ );
458
+
459
+ // Selection computed
460
+ readonly allSelected = computed(() => {
461
+ const page = this.paginatedData();
462
+ if (page.length === 0) return false;
463
+ const selected = this.selectedRows();
464
+ return page.every((row) => this.isRowInList(row, selected));
465
+ });
466
+
467
+ readonly someSelected = computed(() => {
468
+ const page = this.paginatedData();
469
+ const selected = this.selectedRows();
470
+ return page.some((row) => this.isRowInList(row, selected));
471
+ });
472
+
473
+ constructor() {
474
+ effect(() => {
475
+ const config = this.paginationConfig();
476
+ untracked(() => this.pageSize.set(config.pageSize));
477
+ });
478
+
479
+ effect(() => {
480
+ this.filterText();
481
+ this.pageSize();
482
+ this.data();
483
+ untracked(() => this.currentPage.set(1));
484
+ });
485
+ }
486
+
487
+ // Sort
488
+ toggleSort(key: string): void {
489
+ const current = this.sortState();
490
+ let direction: SortDirection;
491
+ if (current.key !== key) {
492
+ direction = 'asc';
493
+ } else if (current.direction === 'asc') {
494
+ direction = 'desc';
495
+ } else if (current.direction === 'desc') {
496
+ direction = null;
497
+ } else {
498
+ direction = 'asc';
499
+ }
500
+ const next: SortState = { key: direction ? key : '', direction };
501
+ this.sortState.set(next);
502
+ this.sortChanged.emit(next);
503
+ }
504
+
505
+ // Filter
506
+ onFilterInput(event: Event): void {
507
+ this.filterText.set((event.target as HTMLInputElement).value);
508
+ }
509
+
510
+ // Page size
511
+ onPageSizeChange(value: string): void {
512
+ this.pageSize.set(Number(value));
513
+ }
514
+
515
+ // Selection
516
+ toggleSelectAll(): void {
517
+ if (this.allSelected()) {
518
+ const page = this.paginatedData();
519
+ this.selectedRows.update((sel) =>
520
+ sel.filter((r) => !page.some((p) => this.rowsEqual(r, p)))
521
+ );
522
+ } else {
523
+ const page = this.paginatedData();
524
+ this.selectedRows.update((sel) => {
525
+ const newSel = [...sel];
526
+ for (const row of page) {
527
+ if (!this.isRowInList(row, newSel)) newSel.push(row);
528
+ }
529
+ return newSel;
530
+ });
531
+ }
532
+ }
533
+
534
+ toggleRowSelection(row: Record<string, unknown>): void {
535
+ this.selectedRows.update((sel) =>
536
+ this.isRowInList(row, sel)
537
+ ? sel.filter((r) => !this.rowsEqual(r, row))
538
+ : [...sel, row]
539
+ );
540
+ }
541
+
542
+ // Row click
543
+ onRowClick(row: Record<string, unknown>): void {
544
+ this.rowClicked.emit(row);
545
+ }
546
+
547
+ // Export
548
+ onExport(): void {
549
+ this.dataExported.emit(this.filteredData());
550
+ }
551
+
552
+ // Column visibility
553
+ toggleColumnVisibility(key: string): void {
554
+ this.hiddenColumns.update((set) => {
555
+ const next = new Set(set);
556
+ if (next.has(key)) next.delete(key);
557
+ else next.add(key);
558
+ return next;
559
+ });
560
+ }
561
+
562
+ // Expansion
563
+ toggleRowExpansion(row: Record<string, unknown>): void {
564
+ const key = this.trackBy() ? row[this.trackBy()] : row;
565
+ this.expandedRows.update((set) => {
566
+ const next = new Set(set);
567
+ if (next.has(key)) next.delete(key);
568
+ else next.add(key);
569
+ return next;
570
+ });
571
+ }
572
+
573
+ isExpanded(row: Record<string, unknown>): boolean {
574
+ const key = this.trackBy() ? row[this.trackBy()] : row;
575
+ return this.expandedRows().has(key);
576
+ }
577
+
578
+ // Helpers
579
+ isSelected(row: Record<string, unknown>): boolean {
580
+ return this.isRowInList(row, this.selectedRows());
581
+ }
582
+
583
+ trackByFn(row: Record<string, unknown>, index: number): unknown {
584
+ const key = this.trackBy();
585
+ return key ? row[key] : index;
586
+ }
587
+
588
+ private isRowInList(
589
+ row: Record<string, unknown>,
590
+ list: Record<string, unknown>[]
591
+ ): boolean {
592
+ return list.some((r) => this.rowsEqual(r, row));
593
+ }
594
+
595
+ private rowsEqual(
596
+ a: Record<string, unknown>,
597
+ b: Record<string, unknown>
598
+ ): boolean {
599
+ const key = this.trackBy();
600
+ if (key) return a[key] === b[key];
601
+ return a === b;
602
+ }
603
+ }
@@ -0,0 +1,35 @@
1
+ import { Directive, TemplateRef, inject, input } from '@angular/core';
2
+
3
+ @Directive({
4
+ selector: '[snyCell]',
5
+ standalone: true,
6
+ })
7
+ export class SnyCellDefDirective {
8
+ readonly snyCell = input.required<string>();
9
+ readonly template = inject(TemplateRef);
10
+ }
11
+
12
+ @Directive({
13
+ selector: '[snyHeaderCell]',
14
+ standalone: true,
15
+ })
16
+ export class SnyHeaderCellDefDirective {
17
+ readonly snyHeaderCell = input.required<string>();
18
+ readonly template = inject(TemplateRef);
19
+ }
20
+
21
+ @Directive({
22
+ selector: '[snyBulkActions]',
23
+ standalone: true,
24
+ })
25
+ export class SnyBulkActionsDefDirective {
26
+ readonly template = inject(TemplateRef);
27
+ }
28
+
29
+ @Directive({
30
+ selector: '[snyRowExpand]',
31
+ standalone: true,
32
+ })
33
+ export class SnyRowExpandDefDirective {
34
+ readonly template = inject(TemplateRef);
35
+ }
@@ -0,0 +1,20 @@
1
+ export interface DataTableColumn {
2
+ key: string;
3
+ label: string;
4
+ sortable?: boolean;
5
+ filterable?: boolean;
6
+ width?: string;
7
+ visible?: boolean;
8
+ }
9
+
10
+ export type SortDirection = 'asc' | 'desc' | null;
11
+
12
+ export interface SortState {
13
+ key: string;
14
+ direction: SortDirection;
15
+ }
16
+
17
+ export interface DataTablePaginationConfig {
18
+ pageSize: number;
19
+ pageSizeOptions: number[];
20
+ }
@@ -0,0 +1,13 @@
1
+ export { SnyDataTableComponent } from './data-table.component';
2
+ export {
3
+ SnyCellDefDirective,
4
+ SnyHeaderCellDefDirective,
5
+ SnyBulkActionsDefDirective,
6
+ SnyRowExpandDefDirective,
7
+ } from './data-table.directives';
8
+ export type {
9
+ DataTableColumn,
10
+ SortState,
11
+ SortDirection,
12
+ DataTablePaginationConfig,
13
+ } from './data-table.types';