@sonny-ui/core 0.1.0-alpha.2 → 0.1.0-alpha.21

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