@surfnet/curve-angular 0.2.0

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 (189) hide show
  1. package/.postcssrc.json +7 -0
  2. package/.storybook/main.ts +9 -0
  3. package/.storybook/manager.ts +3 -0
  4. package/.storybook/preview.ts +28 -0
  5. package/.storybook/tsconfig.json +8 -0
  6. package/.turbo/turbo-build.log +32 -0
  7. package/CHANGELOG.md +29 -0
  8. package/angular.json +52 -0
  9. package/components.json +5 -0
  10. package/dist/fesm2022/surfnet-curve-angular.mjs +4237 -0
  11. package/dist/fesm2022/surfnet-curve-angular.mjs.map +1 -0
  12. package/dist/files/geist-cyrillic-ext-wght-italic.woff2 +0 -0
  13. package/dist/files/geist-cyrillic-ext-wght-normal.woff2 +0 -0
  14. package/dist/files/geist-cyrillic-wght-italic.woff2 +0 -0
  15. package/dist/files/geist-cyrillic-wght-normal.woff2 +0 -0
  16. package/dist/files/geist-latin-ext-wght-italic.woff2 +0 -0
  17. package/dist/files/geist-latin-ext-wght-normal.woff2 +0 -0
  18. package/dist/files/geist-latin-wght-italic.woff2 +0 -0
  19. package/dist/files/geist-latin-wght-normal.woff2 +0 -0
  20. package/dist/files/geist-vietnamese-wght-italic.woff2 +0 -0
  21. package/dist/files/geist-vietnamese-wght-normal.woff2 +0 -0
  22. package/dist/styles.css +2 -0
  23. package/dist/types/surfnet-curve-angular.d.ts +1017 -0
  24. package/ng-package.json +20 -0
  25. package/package.json +73 -0
  26. package/scripts/copy-font-files.ts +15 -0
  27. package/scripts/rewrite-helm-imports.ts +60 -0
  28. package/src/foundations/design-tokens.stories.ts +198 -0
  29. package/src/lib/ui/avatar/src/index.ts +22 -0
  30. package/src/lib/ui/avatar/src/lib/hlm-avatar-badge.ts +19 -0
  31. package/src/lib/ui/avatar/src/lib/hlm-avatar-fallback.ts +20 -0
  32. package/src/lib/ui/avatar/src/lib/hlm-avatar-group-count.ts +17 -0
  33. package/src/lib/ui/avatar/src/lib/hlm-avatar-group.ts +17 -0
  34. package/src/lib/ui/avatar/src/lib/hlm-avatar-image.ts +19 -0
  35. package/src/lib/ui/avatar/src/lib/hlm-avatar.stories.ts +107 -0
  36. package/src/lib/ui/avatar/src/lib/hlm-avatar.ts +32 -0
  37. package/src/lib/ui/breadcrumb/src/index.ts +30 -0
  38. package/src/lib/ui/breadcrumb/src/lib/hlm-breadcrumb-ellipsis.ts +36 -0
  39. package/src/lib/ui/breadcrumb/src/lib/hlm-breadcrumb-item.ts +14 -0
  40. package/src/lib/ui/breadcrumb/src/lib/hlm-breadcrumb-link.ts +36 -0
  41. package/src/lib/ui/breadcrumb/src/lib/hlm-breadcrumb-list.ts +17 -0
  42. package/src/lib/ui/breadcrumb/src/lib/hlm-breadcrumb-page.ts +17 -0
  43. package/src/lib/ui/breadcrumb/src/lib/hlm-breadcrumb-separator.ts +27 -0
  44. package/src/lib/ui/breadcrumb/src/lib/hlm-breadcrumb.stories.ts +81 -0
  45. package/src/lib/ui/breadcrumb/src/lib/hlm-breadcrumb.ts +13 -0
  46. package/src/lib/ui/button/src/index.ts +6 -0
  47. package/src/lib/ui/button/src/lib/hlm-button.stories.ts +170 -0
  48. package/src/lib/ui/button/src/lib/hlm-button.token.ts +22 -0
  49. package/src/lib/ui/button/src/lib/hlm-button.ts +76 -0
  50. package/src/lib/ui/card/src/index.ts +25 -0
  51. package/src/lib/ui/card/src/lib/hlm-card-action.ts +12 -0
  52. package/src/lib/ui/card/src/lib/hlm-card-content.ts +12 -0
  53. package/src/lib/ui/card/src/lib/hlm-card-description.ts +12 -0
  54. package/src/lib/ui/card/src/lib/hlm-card-footer.ts +14 -0
  55. package/src/lib/ui/card/src/lib/hlm-card-header.ts +15 -0
  56. package/src/lib/ui/card/src/lib/hlm-card-title.ts +12 -0
  57. package/src/lib/ui/card/src/lib/hlm-card.stories.ts +105 -0
  58. package/src/lib/ui/card/src/lib/hlm-card.token.ts +19 -0
  59. package/src/lib/ui/card/src/lib/hlm-card.ts +22 -0
  60. package/src/lib/ui/checkbox/src/index.ts +5 -0
  61. package/src/lib/ui/checkbox/src/lib/hlm-checkbox.stories.ts +78 -0
  62. package/src/lib/ui/checkbox/src/lib/hlm-checkbox.ts +160 -0
  63. package/src/lib/ui/data-table/src/index.ts +14 -0
  64. package/src/lib/ui/data-table/src/lib/hlm-data-table-content.ts +87 -0
  65. package/src/lib/ui/data-table/src/lib/hlm-data-table-pagination.ts +62 -0
  66. package/src/lib/ui/data-table/src/lib/hlm-data-table-toolbar.ts +16 -0
  67. package/src/lib/ui/data-table/src/lib/hlm-data-table.stories.ts +479 -0
  68. package/src/lib/ui/data-table/src/lib/inject-data-table.ts +86 -0
  69. package/src/lib/ui/dropdown-menu/src/index.ts +45 -0
  70. package/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu-checkbox-indicator.ts +20 -0
  71. package/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu-checkbox.ts +36 -0
  72. package/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu-group.ts +16 -0
  73. package/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu-item-sub-indicator.ts +17 -0
  74. package/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu-item.ts +41 -0
  75. package/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu-label.ts +20 -0
  76. package/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu-radio-indicator.ts +20 -0
  77. package/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu-radio.ts +36 -0
  78. package/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu-separator.ts +14 -0
  79. package/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu-shortcut.ts +14 -0
  80. package/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu-sub.ts +58 -0
  81. package/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu-token.ts +26 -0
  82. package/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu-trigger.ts +49 -0
  83. package/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu.stories.ts +125 -0
  84. package/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu.ts +62 -0
  85. package/src/lib/ui/field/src/index.ts +34 -0
  86. package/src/lib/ui/field/src/lib/hlm-field-content.ts +12 -0
  87. package/src/lib/ui/field/src/lib/hlm-field-description.ts +53 -0
  88. package/src/lib/ui/field/src/lib/hlm-field-error.ts +100 -0
  89. package/src/lib/ui/field/src/lib/hlm-field-group.ts +15 -0
  90. package/src/lib/ui/field/src/lib/hlm-field-label.ts +17 -0
  91. package/src/lib/ui/field/src/lib/hlm-field-legend.ts +17 -0
  92. package/src/lib/ui/field/src/lib/hlm-field-separator.ts +24 -0
  93. package/src/lib/ui/field/src/lib/hlm-field-set.ts +15 -0
  94. package/src/lib/ui/field/src/lib/hlm-field-title.ts +15 -0
  95. package/src/lib/ui/field/src/lib/hlm-field.stories.ts +117 -0
  96. package/src/lib/ui/field/src/lib/hlm-field.ts +49 -0
  97. package/src/lib/ui/icon/src/index.ts +7 -0
  98. package/src/lib/ui/icon/src/lib/hlm-icon.token.ts +20 -0
  99. package/src/lib/ui/icon/src/lib/hlm-icon.ts +42 -0
  100. package/src/lib/ui/input/src/index.ts +5 -0
  101. package/src/lib/ui/input/src/lib/hlm-input.stories.ts +94 -0
  102. package/src/lib/ui/input/src/lib/hlm-input.ts +20 -0
  103. package/src/lib/ui/input-group/src/index.ts +22 -0
  104. package/src/lib/ui/input-group/src/lib/hlm-input-group-addon.ts +40 -0
  105. package/src/lib/ui/input-group/src/lib/hlm-input-group-button.ts +47 -0
  106. package/src/lib/ui/input-group/src/lib/hlm-input-group-input.ts +17 -0
  107. package/src/lib/ui/input-group/src/lib/hlm-input-group-text.ts +14 -0
  108. package/src/lib/ui/input-group/src/lib/hlm-input-group-textarea.ts +17 -0
  109. package/src/lib/ui/input-group/src/lib/hlm-input-group.stories.ts +133 -0
  110. package/src/lib/ui/input-group/src/lib/hlm-input-group.ts +18 -0
  111. package/src/lib/ui/label/src/index.ts +5 -0
  112. package/src/lib/ui/label/src/lib/hlm-label.stories.ts +55 -0
  113. package/src/lib/ui/label/src/lib/hlm-label.ts +17 -0
  114. package/src/lib/ui/select/src/index.ts +52 -0
  115. package/src/lib/ui/select/src/lib/hlm-select-content.ts +44 -0
  116. package/src/lib/ui/select/src/lib/hlm-select-group.ts +14 -0
  117. package/src/lib/ui/select/src/lib/hlm-select-item.ts +36 -0
  118. package/src/lib/ui/select/src/lib/hlm-select-label.ts +14 -0
  119. package/src/lib/ui/select/src/lib/hlm-select-multiple.ts +45 -0
  120. package/src/lib/ui/select/src/lib/hlm-select-placeholder.ts +17 -0
  121. package/src/lib/ui/select/src/lib/hlm-select-portal.ts +8 -0
  122. package/src/lib/ui/select/src/lib/hlm-select-scroll-down.ts +22 -0
  123. package/src/lib/ui/select/src/lib/hlm-select-scroll-up.ts +22 -0
  124. package/src/lib/ui/select/src/lib/hlm-select-separator.ts +14 -0
  125. package/src/lib/ui/select/src/lib/hlm-select-trigger.ts +59 -0
  126. package/src/lib/ui/select/src/lib/hlm-select-value-template.ts +5 -0
  127. package/src/lib/ui/select/src/lib/hlm-select-value.ts +18 -0
  128. package/src/lib/ui/select/src/lib/hlm-select-values-content.ts +9 -0
  129. package/src/lib/ui/select/src/lib/hlm-select-values.ts +5 -0
  130. package/src/lib/ui/select/src/lib/hlm-select.stories.ts +105 -0
  131. package/src/lib/ui/select/src/lib/hlm-select.ts +45 -0
  132. package/src/lib/ui/separator/src/index.ts +5 -0
  133. package/src/lib/ui/separator/src/lib/hlm-separator.stories.ts +88 -0
  134. package/src/lib/ui/separator/src/lib/hlm-separator.ts +19 -0
  135. package/src/lib/ui/sheet/src/index.ts +34 -0
  136. package/src/lib/ui/sheet/src/lib/hlm-sheet-close.ts +9 -0
  137. package/src/lib/ui/sheet/src/lib/hlm-sheet-content.ts +66 -0
  138. package/src/lib/ui/sheet/src/lib/hlm-sheet-description.ts +14 -0
  139. package/src/lib/ui/sheet/src/lib/hlm-sheet-footer.ts +12 -0
  140. package/src/lib/ui/sheet/src/lib/hlm-sheet-header.ts +12 -0
  141. package/src/lib/ui/sheet/src/lib/hlm-sheet-overlay.ts +27 -0
  142. package/src/lib/ui/sheet/src/lib/hlm-sheet-portal.ts +8 -0
  143. package/src/lib/ui/sheet/src/lib/hlm-sheet-title.ts +14 -0
  144. package/src/lib/ui/sheet/src/lib/hlm-sheet-trigger.ts +9 -0
  145. package/src/lib/ui/sheet/src/lib/hlm-sheet.ts +29 -0
  146. package/src/lib/ui/sidebar/src/index.ts +75 -0
  147. package/src/lib/ui/sidebar/src/lib/hlm-sidebar-content.ts +18 -0
  148. package/src/lib/ui/sidebar/src/lib/hlm-sidebar-footer.ts +15 -0
  149. package/src/lib/ui/sidebar/src/lib/hlm-sidebar-group-action.ts +18 -0
  150. package/src/lib/ui/sidebar/src/lib/hlm-sidebar-group-content.ts +15 -0
  151. package/src/lib/ui/sidebar/src/lib/hlm-sidebar-group-label.ts +18 -0
  152. package/src/lib/ui/sidebar/src/lib/hlm-sidebar-group.ts +15 -0
  153. package/src/lib/ui/sidebar/src/lib/hlm-sidebar-header.ts +15 -0
  154. package/src/lib/ui/sidebar/src/lib/hlm-sidebar-input.ts +17 -0
  155. package/src/lib/ui/sidebar/src/lib/hlm-sidebar-inset.ts +15 -0
  156. package/src/lib/ui/sidebar/src/lib/hlm-sidebar-menu-action.ts +24 -0
  157. package/src/lib/ui/sidebar/src/lib/hlm-sidebar-menu-badge.ts +18 -0
  158. package/src/lib/ui/sidebar/src/lib/hlm-sidebar-menu-button.ts +97 -0
  159. package/src/lib/ui/sidebar/src/lib/hlm-sidebar-menu-item.ts +15 -0
  160. package/src/lib/ui/sidebar/src/lib/hlm-sidebar-menu-skeleton.ts +33 -0
  161. package/src/lib/ui/sidebar/src/lib/hlm-sidebar-menu-sub-button.ts +41 -0
  162. package/src/lib/ui/sidebar/src/lib/hlm-sidebar-menu-sub-item.ts +15 -0
  163. package/src/lib/ui/sidebar/src/lib/hlm-sidebar-menu-sub.ts +18 -0
  164. package/src/lib/ui/sidebar/src/lib/hlm-sidebar-menu.ts +15 -0
  165. package/src/lib/ui/sidebar/src/lib/hlm-sidebar-rail.ts +34 -0
  166. package/src/lib/ui/sidebar/src/lib/hlm-sidebar-separator.ts +17 -0
  167. package/src/lib/ui/sidebar/src/lib/hlm-sidebar-trigger.ts +35 -0
  168. package/src/lib/ui/sidebar/src/lib/hlm-sidebar-wrapper.ts +24 -0
  169. package/src/lib/ui/sidebar/src/lib/hlm-sidebar.service.ts +136 -0
  170. package/src/lib/ui/sidebar/src/lib/hlm-sidebar.stories.ts +177 -0
  171. package/src/lib/ui/sidebar/src/lib/hlm-sidebar.token.ts +33 -0
  172. package/src/lib/ui/sidebar/src/lib/hlm-sidebar.ts +139 -0
  173. package/src/lib/ui/skeleton/src/index.ts +5 -0
  174. package/src/lib/ui/skeleton/src/lib/hlm-skeleton.ts +14 -0
  175. package/src/lib/ui/table/src/index.ts +25 -0
  176. package/src/lib/ui/table/src/lib/hlm-table.stories.ts +70 -0
  177. package/src/lib/ui/table/src/lib/hlm-table.ts +129 -0
  178. package/src/lib/ui/textarea/src/index.ts +5 -0
  179. package/src/lib/ui/textarea/src/lib/hlm-textarea.stories.ts +72 -0
  180. package/src/lib/ui/textarea/src/lib/hlm-textarea.ts +21 -0
  181. package/src/lib/ui/tooltip/src/index.ts +5 -0
  182. package/src/lib/ui/tooltip/src/lib/hlm-tooltip.ts +44 -0
  183. package/src/lib/ui/utils/src/index.ts +1 -0
  184. package/src/lib/ui/utils/src/lib/hlm.ts +313 -0
  185. package/src/public-api.ts +24 -0
  186. package/src/styles.css +43 -0
  187. package/tsconfig.json +45 -0
  188. package/tsconfig.lib.json +11 -0
  189. package/tsconfig.lib.prod.json +9 -0
@@ -0,0 +1,479 @@
1
+ import { ChangeDetectionStrategy, Component, computed, input, signal } from '@angular/core';
2
+ import { NgIcon, provideIcons } from '@ng-icons/core';
3
+ import { phosphorArrowsDownUp, phosphorDotsThreeVertical } from '@ng-icons/phosphor-icons/regular';
4
+ import { HlmButton, HlmButtonImports } from '../../../button/src';
5
+ import { HlmCheckbox } from '../../../checkbox/src';
6
+ import { HlmDropdownMenuImports } from '../../../dropdown-menu/src';
7
+ import { HlmInput } from '../../../input/src';
8
+ import { type Meta, type StoryObj } from '@storybook/angular';
9
+ import { dataTableContract } from '@surfnet/curve-contracts';
10
+ import {
11
+ flexRenderComponent,
12
+ type Column,
13
+ type ColumnDef,
14
+ type Row,
15
+ type Table,
16
+ } from '@tanstack/angular-table';
17
+
18
+ import { HlmDataTableContent } from './hlm-data-table-content';
19
+ import { HlmDataTablePagination } from './hlm-data-table-pagination';
20
+ import { HlmDataTableToolbar } from './hlm-data-table-toolbar';
21
+ import { injectDataTable } from './inject-data-table';
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // Shared fixtures
25
+ // ---------------------------------------------------------------------------
26
+
27
+ type Payment = {
28
+ id: string;
29
+ amount: number;
30
+ status: 'pending' | 'processing' | 'success' | 'failed';
31
+ email: string;
32
+ };
33
+
34
+ const data: Payment[] = [
35
+ { id: 'm5gr84i9', amount: 630.44, status: 'success', email: 'michael.mitc@example.com' },
36
+ { id: '3u1reuv4', amount: 767.5, status: 'success', email: 'felicia.reid@example.com' },
37
+ { id: 'derv1ws0', amount: 396.84, status: 'processing', email: 'georgia.young@example.com' },
38
+ { id: '5kma53ae', amount: 475.22, status: 'success', email: 'alma.lawson@example.com' },
39
+ { id: 'bhqecj4p', amount: 275.43, status: 'failed', email: 'dolores.chambers@example.com' },
40
+ ];
41
+
42
+ const currency = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' });
43
+
44
+ // ---------------------------------------------------------------------------
45
+ // Interactive cell components (mounted by FlexRender via flexRenderComponent)
46
+ // ---------------------------------------------------------------------------
47
+
48
+ @Component({
49
+ selector: 'data-table-select-all',
50
+ imports: [HlmCheckbox],
51
+ changeDetection: ChangeDetectionStrategy.OnPush,
52
+ template: `
53
+ <hlm-checkbox
54
+ aria-label="Select all"
55
+ [checked]="table().getIsAllPageRowsSelected()"
56
+ [indeterminate]="table().getIsSomePageRowsSelected()"
57
+ (checkedChange)="table().toggleAllPageRowsSelected($event)"
58
+ />
59
+ `,
60
+ })
61
+ class DataTableSelectAll {
62
+ public readonly table = input.required<Table<Payment>>();
63
+ }
64
+
65
+ @Component({
66
+ selector: 'data-table-select-row',
67
+ imports: [HlmCheckbox],
68
+ changeDetection: ChangeDetectionStrategy.OnPush,
69
+ template: `
70
+ <hlm-checkbox
71
+ aria-label="Select row"
72
+ [checked]="row().getIsSelected()"
73
+ (checkedChange)="row().toggleSelected($event)"
74
+ />
75
+ `,
76
+ })
77
+ class DataTableSelectRow {
78
+ public readonly row = input.required<Row<Payment>>();
79
+ }
80
+
81
+ @Component({
82
+ selector: 'data-table-email-header',
83
+ imports: [HlmButton, NgIcon],
84
+ providers: [provideIcons({ phosphorArrowsDownUp })],
85
+ changeDetection: ChangeDetectionStrategy.OnPush,
86
+ template: `
87
+ <button
88
+ hlmBtn
89
+ variant="ghost"
90
+ size="sm"
91
+ (click)="column().toggleSorting(column().getIsSorted() === 'asc')"
92
+ >
93
+ Email
94
+ <ng-icon name="phosphorArrowsDownUp" data-icon="inline-end" />
95
+ </button>
96
+ `,
97
+ })
98
+ class DataTableEmailHeader {
99
+ public readonly column = input.required<Column<Payment>>();
100
+ }
101
+
102
+ @Component({
103
+ selector: 'data-table-row-actions',
104
+ imports: [HlmButtonImports, HlmDropdownMenuImports, NgIcon],
105
+ providers: [provideIcons({ phosphorDotsThreeVertical })],
106
+ changeDetection: ChangeDetectionStrategy.OnPush,
107
+ template: `
108
+ <button
109
+ hlmBtn
110
+ variant="ghost"
111
+ size="icon-sm"
112
+ align="end"
113
+ aria-label="Open menu"
114
+ [hlmDropdownMenuTrigger]="menu"
115
+ >
116
+ <ng-icon name="phosphorDotsThreeVertical" />
117
+ </button>
118
+ <ng-template #menu>
119
+ <hlm-dropdown-menu class="w-48">
120
+ <hlm-dropdown-menu-group>
121
+ <hlm-dropdown-menu-label>Actions</hlm-dropdown-menu-label>
122
+ <button hlmDropdownMenuItem (triggered)="copyId()">Copy payment ID</button>
123
+ </hlm-dropdown-menu-group>
124
+ <hlm-dropdown-menu-separator />
125
+ <button hlmDropdownMenuItem>View customer</button>
126
+ <button hlmDropdownMenuItem>View payment details</button>
127
+ </hlm-dropdown-menu>
128
+ </ng-template>
129
+ `,
130
+ })
131
+ class DataTableRowActions {
132
+ public readonly payment = input.required<Payment>();
133
+
134
+ protected copyId(): void {
135
+ void navigator.clipboard?.writeText(this.payment().id);
136
+ }
137
+ }
138
+
139
+ // ---------------------------------------------------------------------------
140
+ // Presentational cell components (the Angular equivalent of React's JSX cells)
141
+ // ---------------------------------------------------------------------------
142
+
143
+ @Component({
144
+ selector: 'data-table-status-cell',
145
+ changeDetection: ChangeDetectionStrategy.OnPush,
146
+ template: `<span class="capitalize">{{ status() }}</span>`,
147
+ })
148
+ class DataTableStatusCell {
149
+ public readonly status = input.required<string>();
150
+ }
151
+
152
+ @Component({
153
+ selector: 'data-table-email-cell',
154
+ changeDetection: ChangeDetectionStrategy.OnPush,
155
+ template: `<span class="lowercase">{{ email() }}</span>`,
156
+ })
157
+ class DataTableEmailCell {
158
+ public readonly email = input.required<string>();
159
+ }
160
+
161
+ @Component({
162
+ selector: 'data-table-amount-header',
163
+ changeDetection: ChangeDetectionStrategy.OnPush,
164
+ template: `<div class="text-right">Amount</div>`,
165
+ })
166
+ class DataTableAmountHeader {}
167
+
168
+ @Component({
169
+ selector: 'data-table-amount-cell',
170
+ changeDetection: ChangeDetectionStrategy.OnPush,
171
+ template: `<div class="text-right font-medium">{{ formatted() }}</div>`,
172
+ })
173
+ class DataTableAmountCell {
174
+ public readonly amount = input.required<number>();
175
+ protected readonly formatted = computed(() => currency.format(this.amount()));
176
+ }
177
+
178
+ // ---------------------------------------------------------------------------
179
+ // Shared columns
180
+ // ---------------------------------------------------------------------------
181
+
182
+ const columns: ColumnDef<Payment, unknown>[] = [
183
+ {
184
+ id: 'select',
185
+ header: ({ table }) => flexRenderComponent(DataTableSelectAll, { inputs: { table } }),
186
+ cell: ({ row }) => flexRenderComponent(DataTableSelectRow, { inputs: { row } }),
187
+ enableSorting: false,
188
+ enableHiding: false,
189
+ },
190
+ {
191
+ accessorKey: 'status',
192
+ header: 'Status',
193
+ cell: ({ row }) =>
194
+ flexRenderComponent(DataTableStatusCell, {
195
+ inputs: { status: row.getValue<string>('status') },
196
+ }),
197
+ },
198
+ {
199
+ accessorKey: 'email',
200
+ header: ({ column }) => flexRenderComponent(DataTableEmailHeader, { inputs: { column } }),
201
+ cell: ({ row }) =>
202
+ flexRenderComponent(DataTableEmailCell, { inputs: { email: row.getValue<string>('email') } }),
203
+ },
204
+ {
205
+ accessorKey: 'amount',
206
+ header: () => flexRenderComponent(DataTableAmountHeader),
207
+ cell: ({ row }) =>
208
+ flexRenderComponent(DataTableAmountCell, {
209
+ inputs: { amount: row.getValue<number>('amount') },
210
+ }),
211
+ },
212
+ {
213
+ id: 'actions',
214
+ enableHiding: false,
215
+ cell: ({ row }) =>
216
+ flexRenderComponent(DataTableRowActions, { inputs: { payment: row.original } }),
217
+ },
218
+ ];
219
+
220
+ // ---------------------------------------------------------------------------
221
+ // Host components (one per story, mirroring the React render functions)
222
+ // ---------------------------------------------------------------------------
223
+
224
+ @Component({
225
+ selector: 'payment-data-table',
226
+ imports: [
227
+ HlmDataTableContent,
228
+ HlmDataTablePagination,
229
+ HlmDataTableToolbar,
230
+ HlmButton,
231
+ HlmInput,
232
+ HlmDropdownMenuImports,
233
+ ],
234
+ changeDetection: ChangeDetectionStrategy.OnPush,
235
+ host: { class: 'block w-full' },
236
+ template: `
237
+ <div hlmDataTableToolbar>
238
+ <input
239
+ hlmInput
240
+ class="max-w-sm"
241
+ placeholder="Filter emails…"
242
+ [value]="emailFilter()"
243
+ (input)="setEmailFilter($event)"
244
+ />
245
+ <button hlmBtn variant="outline" class="ml-auto" [hlmDropdownMenuTrigger]="columnsMenu">
246
+ Columns
247
+ </button>
248
+ <ng-template #columnsMenu>
249
+ <hlm-dropdown-menu class="w-40">
250
+ @for (column of hideableColumns(); track column.id) {
251
+ <button
252
+ hlmDropdownMenuCheckbox
253
+ class="capitalize"
254
+ [checked]="column.getIsVisible()"
255
+ (triggered)="column.toggleVisibility(!column.getIsVisible())"
256
+ >
257
+ {{ column.id }}
258
+ <hlm-dropdown-menu-checkbox-indicator />
259
+ </button>
260
+ }
261
+ </hlm-dropdown-menu>
262
+ </ng-template>
263
+ </div>
264
+ <hlm-data-table-content [table]="table" [columns]="columns" />
265
+ <hlm-data-table-pagination [table]="table" />
266
+ `,
267
+ })
268
+ class PaymentDataTable {
269
+ protected readonly _data = signal(data);
270
+ protected readonly columns = columns;
271
+ protected readonly table = injectDataTable(() => ({ data: this._data(), columns: this.columns }));
272
+
273
+ protected readonly emailFilter = signal('');
274
+
275
+ protected hideableColumns() {
276
+ return this.table.getAllColumns().filter((column) => column.getCanHide());
277
+ }
278
+
279
+ protected setEmailFilter(event: Event): void {
280
+ const value = (event.target as HTMLInputElement).value;
281
+ this.emailFilter.set(value);
282
+ this.table.getColumn('email')?.setFilterValue(value);
283
+ }
284
+ }
285
+
286
+ @Component({
287
+ selector: 'empty-data-table',
288
+ imports: [HlmDataTableContent],
289
+ changeDetection: ChangeDetectionStrategy.OnPush,
290
+ host: { class: 'block w-full' },
291
+ template: `
292
+ <hlm-data-table-content
293
+ [table]="table"
294
+ [columns]="columns"
295
+ noResultsLabel="No payments found."
296
+ />
297
+ `,
298
+ })
299
+ class EmptyDataTable {
300
+ protected readonly columns = columns;
301
+ protected readonly table = injectDataTable<Payment>(() => ({ data: [], columns: this.columns }));
302
+ }
303
+
304
+ @Component({
305
+ selector: 'custom-toolbar-data-table',
306
+ imports: [HlmDataTableContent, HlmDataTablePagination, HlmDataTableToolbar, HlmButton],
307
+ changeDetection: ChangeDetectionStrategy.OnPush,
308
+ host: { class: 'block w-full' },
309
+ template: `
310
+ <div hlmDataTableToolbar class="justify-between">
311
+ <span class="font-medium">Recent payments</span>
312
+ <button hlmBtn size="sm">Export</button>
313
+ </div>
314
+ <hlm-data-table-content [table]="table" [columns]="columns" />
315
+ <hlm-data-table-pagination [table]="table" />
316
+ `,
317
+ })
318
+ class CustomToolbarDataTable {
319
+ protected readonly _data = signal(data);
320
+ protected readonly columns = columns;
321
+ protected readonly table = injectDataTable(() => ({ data: this._data(), columns: this.columns }));
322
+ }
323
+
324
+ // ---------------------------------------------------------------------------
325
+ // Meta + stories
326
+ // ---------------------------------------------------------------------------
327
+
328
+ const meta: Meta = {
329
+ title: 'Components/DataTable',
330
+ parameters: {
331
+ docs: {
332
+ description: {
333
+ component: dataTableContract.docs.description,
334
+ },
335
+ },
336
+ },
337
+ };
338
+
339
+ export default meta;
340
+ type Story = StoryObj;
341
+
342
+ /**
343
+ * Full table: filter input, column-visibility toggle, row selection, sortable Email column,
344
+ * right-aligned Amount, row actions, and Previous/Next pagination.
345
+ */
346
+ export const Default: Story = {
347
+ parameters: {
348
+ docs: {
349
+ source: {
350
+ language: 'typescript',
351
+ code: `@Component({
352
+ selector: 'payment-data-table',
353
+ imports: [
354
+ HlmDataTableContent,
355
+ HlmDataTablePagination,
356
+ HlmDataTableToolbar,
357
+ HlmButton,
358
+ HlmInput,
359
+ HlmDropdownMenuImports,
360
+ ],
361
+ host: { class: 'block w-full' },
362
+ template: \`
363
+ <div hlmDataTableToolbar>
364
+ <input
365
+ hlmInput
366
+ class="max-w-sm"
367
+ placeholder="Filter emails…"
368
+ [value]="emailFilter()"
369
+ (input)="setEmailFilter($event)"
370
+ />
371
+ <button hlmBtn variant="outline" class="ml-auto" [hlmDropdownMenuTrigger]="columnsMenu">
372
+ Columns
373
+ </button>
374
+ <ng-template #columnsMenu>
375
+ <hlm-dropdown-menu class="w-40">
376
+ @for (column of hideableColumns(); track column.id) {
377
+ <button
378
+ hlmDropdownMenuCheckbox
379
+ class="capitalize"
380
+ [checked]="column.getIsVisible()"
381
+ (triggered)="column.toggleVisibility(!column.getIsVisible())"
382
+ >
383
+ {{ column.id }}
384
+ <hlm-dropdown-menu-checkbox-indicator />
385
+ </button>
386
+ }
387
+ </hlm-dropdown-menu>
388
+ </ng-template>
389
+ </div>
390
+ <hlm-data-table-content [table]="table" [columns]="columns" />
391
+ <hlm-data-table-pagination [table]="table" />
392
+ \`,
393
+ })
394
+ class PaymentDataTable {
395
+ protected readonly columns = columns;
396
+ protected readonly table = injectDataTable(() => ({ data, columns: this.columns }));
397
+ protected readonly emailFilter = signal('');
398
+
399
+ protected hideableColumns() {
400
+ return this.table.getAllColumns().filter((column) => column.getCanHide());
401
+ }
402
+
403
+ protected setEmailFilter(event: Event): void {
404
+ const value = (event.target as HTMLInputElement).value;
405
+ this.emailFilter.set(value);
406
+ this.table.getColumn('email')?.setFilterValue(value);
407
+ }
408
+ }`,
409
+ },
410
+ },
411
+ },
412
+ render: () => ({
413
+ moduleMetadata: { imports: [PaymentDataTable] },
414
+ template: '<payment-data-table />',
415
+ }),
416
+ };
417
+
418
+ /** Empty state — no rows to display. */
419
+ export const Empty: Story = {
420
+ parameters: {
421
+ docs: {
422
+ source: {
423
+ language: 'typescript',
424
+ code: `@Component({
425
+ selector: 'empty-data-table',
426
+ imports: [HlmDataTableContent],
427
+ host: { class: 'block w-full' },
428
+ template: \`
429
+ <hlm-data-table-content
430
+ [table]="table"
431
+ [columns]="columns"
432
+ noResultsLabel="No payments found."
433
+ />
434
+ \`,
435
+ })
436
+ class EmptyDataTable {
437
+ protected readonly columns = columns;
438
+ protected readonly table = injectDataTable<Payment>(() => ({ data: [], columns: this.columns }));
439
+ }`,
440
+ },
441
+ },
442
+ },
443
+ render: () => ({
444
+ moduleMetadata: { imports: [EmptyDataTable] },
445
+ template: '<empty-data-table />',
446
+ }),
447
+ };
448
+
449
+ /** Custom toolbar layout: heading on the left, action button on the right. */
450
+ export const CustomToolbar: Story = {
451
+ parameters: {
452
+ docs: {
453
+ source: {
454
+ language: 'typescript',
455
+ code: `@Component({
456
+ selector: 'custom-toolbar-data-table',
457
+ imports: [HlmDataTableContent, HlmDataTablePagination, HlmDataTableToolbar, HlmButton],
458
+ host: { class: 'block w-full' },
459
+ template: \`
460
+ <div hlmDataTableToolbar class="justify-between">
461
+ <span class="font-medium">Recent payments</span>
462
+ <button hlmBtn size="sm">Export</button>
463
+ </div>
464
+ <hlm-data-table-content [table]="table" [columns]="columns" />
465
+ <hlm-data-table-pagination [table]="table" />
466
+ \`,
467
+ })
468
+ class CustomToolbarDataTable {
469
+ protected readonly columns = columns;
470
+ protected readonly table = injectDataTable(() => ({ data, columns: this.columns }));
471
+ }`,
472
+ },
473
+ },
474
+ },
475
+ render: () => ({
476
+ moduleMetadata: { imports: [CustomToolbarDataTable] },
477
+ template: '<custom-toolbar-data-table />',
478
+ }),
479
+ };
@@ -0,0 +1,86 @@
1
+ import { signal, type WritableSignal } from '@angular/core';
2
+ import {
3
+ createAngularTable,
4
+ getCoreRowModel,
5
+ getFilteredRowModel,
6
+ getPaginationRowModel,
7
+ getSortedRowModel,
8
+ type ColumnDef,
9
+ type ColumnFiltersState,
10
+ type PaginationState,
11
+ type Row,
12
+ type RowSelectionState,
13
+ type SortingState,
14
+ type Table,
15
+ type Updater,
16
+ type VisibilityState,
17
+ } from '@tanstack/angular-table';
18
+
19
+ interface InjectDataTableOptions<TData> {
20
+ data: TData[];
21
+ columns: ColumnDef<TData, unknown>[];
22
+ /** Derive a stable row id (e.g. from a record's id) so row selection survives data changes. */
23
+ getRowId?: (originalRow: TData, index: number, parent?: Row<TData>) => string;
24
+ initialSorting?: SortingState;
25
+ initialColumnFilters?: ColumnFiltersState;
26
+ initialColumnVisibility?: VisibilityState;
27
+ initialPagination?: PaginationState;
28
+ }
29
+
30
+ /** Apply a TanStack `Updater` (value or `(old) => new`) to a writable signal. */
31
+ function applyUpdater<T>(state: WritableSignal<T>): (updater: Updater<T>) => void {
32
+ return (updater) =>
33
+ state.set(typeof updater === 'function' ? (updater as (old: T) => T)(state()) : updater);
34
+ }
35
+
36
+ /**
37
+ * Angular analog of React's `useDataTable`. Wraps `createAngularTable` with the core,
38
+ * sorted, filtered and paginated row models and manages sorting, column-filter,
39
+ * column-visibility, row-selection and pagination state with signals.
40
+ *
41
+ * Must be called from an injection context (a component constructor or field initializer),
42
+ * since `createAngularTable` injects. Pass an options factory so reactive `data` re-renders
43
+ * the table:
44
+ *
45
+ * ```ts
46
+ * readonly table = injectDataTable(() => ({ data: this.data(), columns }));
47
+ * ```
48
+ */
49
+ function injectDataTable<TData>(options: () => InjectDataTableOptions<TData>): Table<TData> {
50
+ const initial = options();
51
+
52
+ const sorting = signal<SortingState>(initial.initialSorting ?? []);
53
+ const columnFilters = signal<ColumnFiltersState>(initial.initialColumnFilters ?? []);
54
+ const columnVisibility = signal<VisibilityState>(initial.initialColumnVisibility ?? {});
55
+ const rowSelection = signal<RowSelectionState>({});
56
+ const pagination = signal<PaginationState>(
57
+ initial.initialPagination ?? { pageIndex: 0, pageSize: 10 },
58
+ );
59
+
60
+ return createAngularTable(() => {
61
+ const { data, columns, getRowId } = options();
62
+ return {
63
+ data,
64
+ columns,
65
+ getRowId,
66
+ state: {
67
+ sorting: sorting(),
68
+ columnFilters: columnFilters(),
69
+ columnVisibility: columnVisibility(),
70
+ rowSelection: rowSelection(),
71
+ pagination: pagination(),
72
+ },
73
+ onSortingChange: applyUpdater(sorting),
74
+ onColumnFiltersChange: applyUpdater(columnFilters),
75
+ onColumnVisibilityChange: applyUpdater(columnVisibility),
76
+ onRowSelectionChange: applyUpdater(rowSelection),
77
+ onPaginationChange: applyUpdater(pagination),
78
+ getCoreRowModel: getCoreRowModel(),
79
+ getSortedRowModel: getSortedRowModel(),
80
+ getFilteredRowModel: getFilteredRowModel(),
81
+ getPaginationRowModel: getPaginationRowModel(),
82
+ };
83
+ });
84
+ }
85
+
86
+ export { injectDataTable, type InjectDataTableOptions };
@@ -0,0 +1,45 @@
1
+ import { HlmDropdownMenu } from './lib/hlm-dropdown-menu';
2
+ import { HlmDropdownMenuCheckbox } from './lib/hlm-dropdown-menu-checkbox';
3
+ import { HlmDropdownMenuCheckboxIndicator } from './lib/hlm-dropdown-menu-checkbox-indicator';
4
+ import { HlmDropdownMenuGroup } from './lib/hlm-dropdown-menu-group';
5
+ import { HlmDropdownMenuItem } from './lib/hlm-dropdown-menu-item';
6
+ import { HlmDropdownMenuItemSubIndicator } from './lib/hlm-dropdown-menu-item-sub-indicator';
7
+ import { HlmDropdownMenuLabel } from './lib/hlm-dropdown-menu-label';
8
+ import { HlmDropdownMenuRadio } from './lib/hlm-dropdown-menu-radio';
9
+ import { HlmDropdownMenuRadioIndicator } from './lib/hlm-dropdown-menu-radio-indicator';
10
+ import { HlmDropdownMenuSeparator } from './lib/hlm-dropdown-menu-separator';
11
+ import { HlmDropdownMenuShortcut } from './lib/hlm-dropdown-menu-shortcut';
12
+ import { HlmDropdownMenuSub } from './lib/hlm-dropdown-menu-sub';
13
+
14
+ import { HlmDropdownMenuTrigger } from './lib/hlm-dropdown-menu-trigger';
15
+
16
+ export * from './lib/hlm-dropdown-menu';
17
+ export * from './lib/hlm-dropdown-menu-checkbox';
18
+ export * from './lib/hlm-dropdown-menu-checkbox-indicator';
19
+ export * from './lib/hlm-dropdown-menu-group';
20
+ export * from './lib/hlm-dropdown-menu-item';
21
+ export * from './lib/hlm-dropdown-menu-item-sub-indicator';
22
+ export * from './lib/hlm-dropdown-menu-label';
23
+ export * from './lib/hlm-dropdown-menu-radio';
24
+ export * from './lib/hlm-dropdown-menu-radio-indicator';
25
+ export * from './lib/hlm-dropdown-menu-separator';
26
+ export * from './lib/hlm-dropdown-menu-shortcut';
27
+ export * from './lib/hlm-dropdown-menu-sub';
28
+ export * from './lib/hlm-dropdown-menu-token';
29
+ export * from './lib/hlm-dropdown-menu-trigger';
30
+
31
+ export const HlmDropdownMenuImports = [
32
+ HlmDropdownMenu,
33
+ HlmDropdownMenuCheckbox,
34
+ HlmDropdownMenuCheckboxIndicator,
35
+ HlmDropdownMenuGroup,
36
+ HlmDropdownMenuItem,
37
+ HlmDropdownMenuItemSubIndicator,
38
+ HlmDropdownMenuLabel,
39
+ HlmDropdownMenuRadio,
40
+ HlmDropdownMenuRadioIndicator,
41
+ HlmDropdownMenuSeparator,
42
+ HlmDropdownMenuShortcut,
43
+ HlmDropdownMenuSub,
44
+ HlmDropdownMenuTrigger,
45
+ ] as const;
@@ -0,0 +1,20 @@
1
+ import { ChangeDetectionStrategy, Component } from '@angular/core';
2
+ import { NgIcon, provideIcons } from '@ng-icons/core';
3
+ import { phosphorCheck } from '@ng-icons/phosphor-icons/regular';
4
+ import { classes } from '../../../utils/src';
5
+
6
+ @Component({
7
+ selector: 'hlm-dropdown-menu-checkbox-indicator',
8
+ imports: [NgIcon],
9
+ providers: [provideIcons({ phosphorCheck })],
10
+ changeDetection: ChangeDetectionStrategy.OnPush,
11
+ template: ` <ng-icon class="text-base" name="phosphorCheck" /> `,
12
+ })
13
+ export class HlmDropdownMenuCheckboxIndicator {
14
+ constructor() {
15
+ classes(
16
+ () =>
17
+ 'pointer-events-none absolute left-2 flex size-3.5 items-center justify-center opacity-0 group-data-[checked]:opacity-100',
18
+ );
19
+ }
20
+ }
@@ -0,0 +1,36 @@
1
+ import { type BooleanInput } from '@angular/cdk/coercion';
2
+ import { CdkMenuItemCheckbox } from '@angular/cdk/menu';
3
+ import { Directive, booleanAttribute, inject, input } from '@angular/core';
4
+ import { classes } from '../../../utils/src';
5
+
6
+ @Directive({
7
+ selector: '[hlmDropdownMenuCheckbox]',
8
+ hostDirectives: [
9
+ {
10
+ directive: CdkMenuItemCheckbox,
11
+ inputs: ['cdkMenuItemDisabled: disabled', 'cdkMenuItemChecked: checked'],
12
+ outputs: ['cdkMenuItemTriggered: triggered'],
13
+ },
14
+ ],
15
+ host: {
16
+ 'data-slot': 'dropdown-menu-checkbox-item',
17
+ '[attr.data-disabled]': 'disabled() ? "" : null',
18
+ '[attr.data-checked]': 'checked() ? "" : null',
19
+ },
20
+ })
21
+ export class HlmDropdownMenuCheckbox {
22
+ private readonly _cdkMenuItem = inject(CdkMenuItemCheckbox);
23
+ public readonly checked = input<boolean, BooleanInput>(this._cdkMenuItem.checked, {
24
+ transform: booleanAttribute,
25
+ });
26
+ public readonly disabled = input<boolean, BooleanInput>(this._cdkMenuItem.disabled, {
27
+ transform: booleanAttribute,
28
+ });
29
+
30
+ constructor() {
31
+ classes(
32
+ () =>
33
+ 'hover:bg-accent hover:text-accent-foreground focus-visible:bg-accent focus-visible:text-accent-foreground group relative flex w-full cursor-default items-center rounded-sm py-1.5 pr-2 pl-8 text-sm transition-colors outline-none select-none has-[>hlm-dropdown-menu-checkbox-indicator:last-child]:ps-2 has-[>hlm-dropdown-menu-checkbox-indicator:last-child]:pe-8 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 has-[>hlm-dropdown-menu-checkbox-indicator:last-child]:[&>hlm-dropdown-menu-checkbox-indicator]:start-auto has-[>hlm-dropdown-menu-checkbox-indicator:last-child]:[&>hlm-dropdown-menu-checkbox-indicator]:end-2',
34
+ );
35
+ }
36
+ }
@@ -0,0 +1,16 @@
1
+ import { CdkMenuGroup } from '@angular/cdk/menu';
2
+ import { Directive } from '@angular/core';
3
+ import { classes } from '../../../utils/src';
4
+
5
+ @Directive({
6
+ selector: '[hlmDropdownMenuGroup],hlm-dropdown-menu-group',
7
+ hostDirectives: [CdkMenuGroup],
8
+ host: {
9
+ 'data-slot': 'dropdown-menu-group',
10
+ },
11
+ })
12
+ export class HlmDropdownMenuGroup {
13
+ constructor() {
14
+ classes(() => 'block');
15
+ }
16
+ }