@cdevhub/ngx-tw 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 (171) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +134 -0
  3. package/fesm2022/cdevhub-ngx-tw-accordion.mjs +108 -0
  4. package/fesm2022/cdevhub-ngx-tw-accordion.mjs.map +1 -0
  5. package/fesm2022/cdevhub-ngx-tw-alert.mjs +240 -0
  6. package/fesm2022/cdevhub-ngx-tw-alert.mjs.map +1 -0
  7. package/fesm2022/cdevhub-ngx-tw-avatar.mjs +287 -0
  8. package/fesm2022/cdevhub-ngx-tw-avatar.mjs.map +1 -0
  9. package/fesm2022/cdevhub-ngx-tw-badge.mjs +332 -0
  10. package/fesm2022/cdevhub-ngx-tw-badge.mjs.map +1 -0
  11. package/fesm2022/cdevhub-ngx-tw-breadcrumbs.mjs +452 -0
  12. package/fesm2022/cdevhub-ngx-tw-breadcrumbs.mjs.map +1 -0
  13. package/fesm2022/cdevhub-ngx-tw-button.mjs +227 -0
  14. package/fesm2022/cdevhub-ngx-tw-button.mjs.map +1 -0
  15. package/fesm2022/cdevhub-ngx-tw-calendar-luxon.mjs +417 -0
  16. package/fesm2022/cdevhub-ngx-tw-calendar-luxon.mjs.map +1 -0
  17. package/fesm2022/cdevhub-ngx-tw-calendar-testing.mjs +232 -0
  18. package/fesm2022/cdevhub-ngx-tw-calendar-testing.mjs.map +1 -0
  19. package/fesm2022/cdevhub-ngx-tw-calendar.mjs +4482 -0
  20. package/fesm2022/cdevhub-ngx-tw-calendar.mjs.map +1 -0
  21. package/fesm2022/cdevhub-ngx-tw-card.mjs +157 -0
  22. package/fesm2022/cdevhub-ngx-tw-card.mjs.map +1 -0
  23. package/fesm2022/cdevhub-ngx-tw-carousel.mjs +1454 -0
  24. package/fesm2022/cdevhub-ngx-tw-carousel.mjs.map +1 -0
  25. package/fesm2022/cdevhub-ngx-tw-checkbox.mjs +593 -0
  26. package/fesm2022/cdevhub-ngx-tw-checkbox.mjs.map +1 -0
  27. package/fesm2022/cdevhub-ngx-tw-code-block.mjs +228 -0
  28. package/fesm2022/cdevhub-ngx-tw-code-block.mjs.map +1 -0
  29. package/fesm2022/cdevhub-ngx-tw-collapsible.mjs +510 -0
  30. package/fesm2022/cdevhub-ngx-tw-collapsible.mjs.map +1 -0
  31. package/fesm2022/cdevhub-ngx-tw-combobox.mjs +1721 -0
  32. package/fesm2022/cdevhub-ngx-tw-combobox.mjs.map +1 -0
  33. package/fesm2022/cdevhub-ngx-tw-command-palette.mjs +721 -0
  34. package/fesm2022/cdevhub-ngx-tw-command-palette.mjs.map +1 -0
  35. package/fesm2022/cdevhub-ngx-tw-core.mjs +861 -0
  36. package/fesm2022/cdevhub-ngx-tw-core.mjs.map +1 -0
  37. package/fesm2022/cdevhub-ngx-tw-date-picker.mjs +1558 -0
  38. package/fesm2022/cdevhub-ngx-tw-date-picker.mjs.map +1 -0
  39. package/fesm2022/cdevhub-ngx-tw-date-range-picker.mjs +1833 -0
  40. package/fesm2022/cdevhub-ngx-tw-date-range-picker.mjs.map +1 -0
  41. package/fesm2022/cdevhub-ngx-tw-dialog.mjs +693 -0
  42. package/fesm2022/cdevhub-ngx-tw-dialog.mjs.map +1 -0
  43. package/fesm2022/cdevhub-ngx-tw-empty-state.mjs +331 -0
  44. package/fesm2022/cdevhub-ngx-tw-empty-state.mjs.map +1 -0
  45. package/fesm2022/cdevhub-ngx-tw-flip-card.mjs +289 -0
  46. package/fesm2022/cdevhub-ngx-tw-flip-card.mjs.map +1 -0
  47. package/fesm2022/cdevhub-ngx-tw-form-field.mjs +572 -0
  48. package/fesm2022/cdevhub-ngx-tw-form-field.mjs.map +1 -0
  49. package/fesm2022/cdevhub-ngx-tw-icon-lucide.mjs +36 -0
  50. package/fesm2022/cdevhub-ngx-tw-icon-lucide.mjs.map +1 -0
  51. package/fesm2022/cdevhub-ngx-tw-icon.mjs +278 -0
  52. package/fesm2022/cdevhub-ngx-tw-icon.mjs.map +1 -0
  53. package/fesm2022/cdevhub-ngx-tw-input.mjs +355 -0
  54. package/fesm2022/cdevhub-ngx-tw-input.mjs.map +1 -0
  55. package/fesm2022/cdevhub-ngx-tw-item.mjs +241 -0
  56. package/fesm2022/cdevhub-ngx-tw-item.mjs.map +1 -0
  57. package/fesm2022/cdevhub-ngx-tw-menu.mjs +433 -0
  58. package/fesm2022/cdevhub-ngx-tw-menu.mjs.map +1 -0
  59. package/fesm2022/cdevhub-ngx-tw-paginator.mjs +691 -0
  60. package/fesm2022/cdevhub-ngx-tw-paginator.mjs.map +1 -0
  61. package/fesm2022/cdevhub-ngx-tw-popover.mjs +678 -0
  62. package/fesm2022/cdevhub-ngx-tw-popover.mjs.map +1 -0
  63. package/fesm2022/cdevhub-ngx-tw-progress-bar.mjs +335 -0
  64. package/fesm2022/cdevhub-ngx-tw-progress-bar.mjs.map +1 -0
  65. package/fesm2022/cdevhub-ngx-tw-radio.mjs +708 -0
  66. package/fesm2022/cdevhub-ngx-tw-radio.mjs.map +1 -0
  67. package/fesm2022/cdevhub-ngx-tw-segmented-control.mjs +319 -0
  68. package/fesm2022/cdevhub-ngx-tw-segmented-control.mjs.map +1 -0
  69. package/fesm2022/cdevhub-ngx-tw-select.mjs +1722 -0
  70. package/fesm2022/cdevhub-ngx-tw-select.mjs.map +1 -0
  71. package/fesm2022/cdevhub-ngx-tw-separator.mjs +115 -0
  72. package/fesm2022/cdevhub-ngx-tw-separator.mjs.map +1 -0
  73. package/fesm2022/cdevhub-ngx-tw-sheet.mjs +768 -0
  74. package/fesm2022/cdevhub-ngx-tw-sheet.mjs.map +1 -0
  75. package/fesm2022/cdevhub-ngx-tw-skeleton.mjs +149 -0
  76. package/fesm2022/cdevhub-ngx-tw-skeleton.mjs.map +1 -0
  77. package/fesm2022/cdevhub-ngx-tw-slider.mjs +1147 -0
  78. package/fesm2022/cdevhub-ngx-tw-slider.mjs.map +1 -0
  79. package/fesm2022/cdevhub-ngx-tw-sort.mjs +346 -0
  80. package/fesm2022/cdevhub-ngx-tw-sort.mjs.map +1 -0
  81. package/fesm2022/cdevhub-ngx-tw-spinner.mjs +186 -0
  82. package/fesm2022/cdevhub-ngx-tw-spinner.mjs.map +1 -0
  83. package/fesm2022/cdevhub-ngx-tw-split.mjs +1082 -0
  84. package/fesm2022/cdevhub-ngx-tw-split.mjs.map +1 -0
  85. package/fesm2022/cdevhub-ngx-tw-stat.mjs +579 -0
  86. package/fesm2022/cdevhub-ngx-tw-stat.mjs.map +1 -0
  87. package/fesm2022/cdevhub-ngx-tw-stepper.mjs +443 -0
  88. package/fesm2022/cdevhub-ngx-tw-stepper.mjs.map +1 -0
  89. package/fesm2022/cdevhub-ngx-tw-switch.mjs +391 -0
  90. package/fesm2022/cdevhub-ngx-tw-switch.mjs.map +1 -0
  91. package/fesm2022/cdevhub-ngx-tw-tab-nav.mjs +315 -0
  92. package/fesm2022/cdevhub-ngx-tw-tab-nav.mjs.map +1 -0
  93. package/fesm2022/cdevhub-ngx-tw-table.mjs +994 -0
  94. package/fesm2022/cdevhub-ngx-tw-table.mjs.map +1 -0
  95. package/fesm2022/cdevhub-ngx-tw-tabs.mjs +508 -0
  96. package/fesm2022/cdevhub-ngx-tw-tabs.mjs.map +1 -0
  97. package/fesm2022/cdevhub-ngx-tw-textarea.mjs +147 -0
  98. package/fesm2022/cdevhub-ngx-tw-textarea.mjs.map +1 -0
  99. package/fesm2022/cdevhub-ngx-tw-theme.mjs +178 -0
  100. package/fesm2022/cdevhub-ngx-tw-theme.mjs.map +1 -0
  101. package/fesm2022/cdevhub-ngx-tw-time-picker.mjs +1344 -0
  102. package/fesm2022/cdevhub-ngx-tw-time-picker.mjs.map +1 -0
  103. package/fesm2022/cdevhub-ngx-tw-timeline.mjs +1136 -0
  104. package/fesm2022/cdevhub-ngx-tw-timeline.mjs.map +1 -0
  105. package/fesm2022/cdevhub-ngx-tw-toast.mjs +1311 -0
  106. package/fesm2022/cdevhub-ngx-tw-toast.mjs.map +1 -0
  107. package/fesm2022/cdevhub-ngx-tw-tooltip.mjs +533 -0
  108. package/fesm2022/cdevhub-ngx-tw-tooltip.mjs.map +1 -0
  109. package/fesm2022/cdevhub-ngx-tw.mjs +61 -0
  110. package/fesm2022/cdevhub-ngx-tw.mjs.map +1 -0
  111. package/package.json +265 -0
  112. package/theme/_base.css +394 -0
  113. package/theme/_dark.css +510 -0
  114. package/theme/_high-contrast.css +290 -0
  115. package/theme/_semantic.css +317 -0
  116. package/theme/_typography.css +9 -0
  117. package/theme/index.css +54 -0
  118. package/types/cdevhub-ngx-tw-accordion.d.ts +48 -0
  119. package/types/cdevhub-ngx-tw-alert.d.ts +67 -0
  120. package/types/cdevhub-ngx-tw-avatar.d.ts +79 -0
  121. package/types/cdevhub-ngx-tw-badge.d.ts +94 -0
  122. package/types/cdevhub-ngx-tw-breadcrumbs.d.ts +227 -0
  123. package/types/cdevhub-ngx-tw-button.d.ts +74 -0
  124. package/types/cdevhub-ngx-tw-calendar-luxon.d.ts +200 -0
  125. package/types/cdevhub-ngx-tw-calendar-testing.d.ts +113 -0
  126. package/types/cdevhub-ngx-tw-calendar.d.ts +1704 -0
  127. package/types/cdevhub-ngx-tw-card.d.ts +48 -0
  128. package/types/cdevhub-ngx-tw-carousel.d.ts +406 -0
  129. package/types/cdevhub-ngx-tw-checkbox.d.ts +136 -0
  130. package/types/cdevhub-ngx-tw-code-block.d.ts +93 -0
  131. package/types/cdevhub-ngx-tw-collapsible.d.ts +151 -0
  132. package/types/cdevhub-ngx-tw-combobox.d.ts +389 -0
  133. package/types/cdevhub-ngx-tw-command-palette.d.ts +215 -0
  134. package/types/cdevhub-ngx-tw-core.d.ts +719 -0
  135. package/types/cdevhub-ngx-tw-date-picker.d.ts +317 -0
  136. package/types/cdevhub-ngx-tw-date-range-picker.d.ts +313 -0
  137. package/types/cdevhub-ngx-tw-dialog.d.ts +303 -0
  138. package/types/cdevhub-ngx-tw-empty-state.d.ts +104 -0
  139. package/types/cdevhub-ngx-tw-flip-card.d.ts +95 -0
  140. package/types/cdevhub-ngx-tw-form-field.d.ts +210 -0
  141. package/types/cdevhub-ngx-tw-icon-lucide.d.ts +29 -0
  142. package/types/cdevhub-ngx-tw-icon.d.ts +134 -0
  143. package/types/cdevhub-ngx-tw-input.d.ts +110 -0
  144. package/types/cdevhub-ngx-tw-item.d.ts +70 -0
  145. package/types/cdevhub-ngx-tw-menu.d.ts +99 -0
  146. package/types/cdevhub-ngx-tw-paginator.d.ts +305 -0
  147. package/types/cdevhub-ngx-tw-popover.d.ts +157 -0
  148. package/types/cdevhub-ngx-tw-progress-bar.d.ts +100 -0
  149. package/types/cdevhub-ngx-tw-radio.d.ts +174 -0
  150. package/types/cdevhub-ngx-tw-segmented-control.d.ts +66 -0
  151. package/types/cdevhub-ngx-tw-select.d.ts +384 -0
  152. package/types/cdevhub-ngx-tw-separator.d.ts +28 -0
  153. package/types/cdevhub-ngx-tw-sheet.d.ts +335 -0
  154. package/types/cdevhub-ngx-tw-skeleton.d.ts +52 -0
  155. package/types/cdevhub-ngx-tw-slider.d.ts +258 -0
  156. package/types/cdevhub-ngx-tw-sort.d.ts +136 -0
  157. package/types/cdevhub-ngx-tw-spinner.d.ts +50 -0
  158. package/types/cdevhub-ngx-tw-split.d.ts +265 -0
  159. package/types/cdevhub-ngx-tw-stat.d.ts +140 -0
  160. package/types/cdevhub-ngx-tw-stepper.d.ts +127 -0
  161. package/types/cdevhub-ngx-tw-switch.d.ts +88 -0
  162. package/types/cdevhub-ngx-tw-tab-nav.d.ts +107 -0
  163. package/types/cdevhub-ngx-tw-table.d.ts +479 -0
  164. package/types/cdevhub-ngx-tw-tabs.d.ts +121 -0
  165. package/types/cdevhub-ngx-tw-textarea.d.ts +62 -0
  166. package/types/cdevhub-ngx-tw-theme.d.ts +106 -0
  167. package/types/cdevhub-ngx-tw-time-picker.d.ts +303 -0
  168. package/types/cdevhub-ngx-tw-timeline.d.ts +278 -0
  169. package/types/cdevhub-ngx-tw-toast.d.ts +388 -0
  170. package/types/cdevhub-ngx-tw-tooltip.d.ts +95 -0
  171. package/types/cdevhub-ngx-tw.d.ts +49 -0
@@ -0,0 +1,994 @@
1
+ import { NgTemplateOutlet } from '@angular/common';
2
+ import * as i0 from '@angular/core';
3
+ import { inject, TemplateRef, Directive, input, contentChild, signal, computed, ChangeDetectionStrategy, Component, model, output, contentChildren, viewChild, ElementRef, DestroyRef, effect, untracked, isDevMode, afterNextRender } from '@angular/core';
4
+ import { LiveAnnouncer } from '@angular/cdk/a11y';
5
+ import * as i1 from '@angular/cdk/table';
6
+ import { CdkNoDataRow, CdkTableModule } from '@angular/cdk/table';
7
+ import { tv } from 'tailwind-variants';
8
+ import { CheckboxComponent } from '@cdevhub/ngx-tw/checkbox';
9
+ import { TW_SORT_HANDLE } from '@cdevhub/ngx-tw/core';
10
+ export { DataSource } from '@angular/cdk/collections';
11
+
12
+ /*
13
+ * tw-table — wraps Angular CDK's CdkTable with a declarative <tw-column> API,
14
+ * slot-based chrome (caption, toolbar, empty, loading, error, footer, pagination),
15
+ * responsive (scroll / stack / hide) + sticky + layout controls, and typed
16
+ * template contexts per cell/row/footer/expansion.
17
+ *
18
+ * v2 input shape — visual/behavioral concerns are grouped into config objects:
19
+ * - `appearance` { variant, density, size, layout, rowAnimations }
20
+ * - `sticky` { header, footer, scrollHeight }
21
+ * - `responsive` { mode, stackBelow }
22
+ * - `selection` { enabled }
23
+ * - `<tw-column>` `display` { sticky, align, numeric, hideBelow, width }
24
+ *
25
+ * Each config input accepts a partial object; unset keys fall back to the documented
26
+ * defaults (see `APPEARANCE_DEFAULTS`, etc.). Data, state, mechanical mode flags, i18n
27
+ * and a11y attributes stay flat.
28
+ *
29
+ * Out of scope for v1:
30
+ * - Column resize, drag-reorder
31
+ * - CDK virtual scroll viewport integration
32
+ * - Inline cell editing
33
+ * - Filter UI primitives
34
+ * - Arrow-key "grid" pattern keyboard navigation (APG)
35
+ */
36
+ // ── Resolved-default constants ───────────────────────────────────────
37
+ const APPEARANCE_DEFAULTS = {
38
+ variant: 'default',
39
+ density: 'comfortable',
40
+ size: 'md',
41
+ layout: 'auto',
42
+ rowAnimations: false,
43
+ };
44
+ const STICKY_DEFAULTS = {
45
+ header: false,
46
+ footer: false,
47
+ scrollHeight: null,
48
+ };
49
+ const RESPONSIVE_DEFAULTS = {
50
+ mode: 'scroll',
51
+ stackBelow: 'md',
52
+ };
53
+ const SELECTION_DEFAULTS = {
54
+ enabled: false,
55
+ };
56
+ const DISPLAY_DEFAULTS = {
57
+ sticky: false,
58
+ align: 'start',
59
+ numeric: false,
60
+ hideBelow: null,
61
+ width: null,
62
+ };
63
+ /** Default English labels for the table. */
64
+ const DEFAULT_TABLE_LABELS = {
65
+ ariaLabel: 'Data table',
66
+ empty: 'No data',
67
+ loading: 'Loading…',
68
+ errorPrefix: 'Error: ',
69
+ rowsUpdatedAnnouncement: '{count} rows loaded',
70
+ selectionAnnouncement: '{count} rows selected',
71
+ expandRowLabel: 'Expand row',
72
+ collapseRowLabel: 'Collapse row',
73
+ selectAllLabel: 'Select all rows',
74
+ selectRowLabel: 'Select row {index}',
75
+ };
76
+ // ── tv() config ──────────────────────────────────────────────────────
77
+ // CDK renders `<thead>` / `<tbody>` / `<tfoot>` inside its native-table template;
78
+ // we can't set classes on those elements directly. Instead, we style them via
79
+ // arbitrary descendant selectors on the `<table>` element itself (`[&>thead]:…`,
80
+ // `[&>tbody>tr]:…`, etc.) — the full class strings stay statically present in
81
+ // source so Tailwind v4's JIT scanner picks them up.
82
+ const tableVariants = tv({
83
+ slots: {
84
+ root: 'relative block w-full',
85
+ toolbar: 'empty:hidden flex items-center justify-between gap-3 px-4 py-3 border-b border-border',
86
+ scrollWrapper: 'relative',
87
+ scrollContainer: 'overflow-x-auto w-full',
88
+ // `border-separate` + `border-spacing-0` is required for `position: sticky` on
89
+ // `<th>` / `<td>` (CDK's sticky mechanism) to work reliably across browsers.
90
+ // In border-collapse mode, Firefox (and historically Chrome) fails to stick
91
+ // individual cells. Consequence: borders on row groups (`<thead>`, `<tfoot>`,
92
+ // `<tr>`) don't render — all separators must be applied to cells directly.
93
+ table: 'w-full border-separate border-spacing-0 text-start align-middle [&>thead>tr>th]:font-semibold [&>thead>tr>th]:text-fg [&>thead>tr>th]:tracking-tight [&>tbody>tr]:transition-colors [&>tbody>tr]:duration-normal [&>tbody>tr]:motion-reduce:transition-none [&>tfoot>tr>td]:border-t [&>tfoot>tr>td]:border-border',
94
+ th: 'text-start font-semibold text-fg tracking-tight align-middle',
95
+ td: 'text-fg align-middle',
96
+ footerTd: 'text-fg font-medium align-middle',
97
+ emptyState: 'flex flex-col items-center justify-center gap-2 px-4 py-12 text-center',
98
+ emptyIcon: 'size-10 shrink-0 text-fg-subtle',
99
+ emptyMessage: 'text-sm text-fg-muted',
100
+ loadingOverlay: 'absolute inset-0 flex items-center justify-center gap-3 bg-surface/70 backdrop-blur-sm z-20 pointer-events-auto',
101
+ loadingMessage: 'text-sm text-fg-muted',
102
+ errorState: 'flex flex-col items-center justify-center gap-2 px-4 py-12 text-center',
103
+ errorIcon: 'size-10 shrink-0 text-error-500',
104
+ errorMessage: 'text-sm text-error-700',
105
+ footerSlot: 'empty:hidden px-4 py-2 border-t border-border text-sm text-fg-muted',
106
+ paginationSlot: 'empty:hidden border-t border-border px-2 py-2',
107
+ expansionRow: 'bg-surface-sunken',
108
+ expansionCell: 'p-0',
109
+ // The leading `_selection` column hosts checkboxes only — override the standard
110
+ // text alignment + padding so the control stays centered in a narrow column.
111
+ selectionHeader: 'w-12 text-center align-middle',
112
+ selectionCell: 'w-12 text-center align-middle',
113
+ },
114
+ variants: {
115
+ variant: {
116
+ default: {
117
+ table: '[&>thead>tr>th]:bg-surface-muted [&>tbody>tr:not(:last-child)>td]:border-b [&>tbody>tr:not(:last-child)>td]:border-border [&>tbody>tr:hover]:bg-surface-sunken',
118
+ },
119
+ striped: {
120
+ table: '[&>thead>tr>th]:bg-surface-muted [&>tbody>tr:nth-child(even)]:bg-surface-sunken [&>tbody>tr:hover]:bg-surface-muted',
121
+ },
122
+ bordered: {
123
+ root: 'rounded-lg border border-border-strong overflow-hidden',
124
+ table: '[&>thead>tr>th]:bg-surface-muted [&>thead>tr>th]:border-b [&>thead>tr>th]:border-border [&>tbody>tr:hover]:bg-surface-sunken',
125
+ th: 'border-r border-border last:border-r-0',
126
+ td: 'border-r border-border last:border-r-0',
127
+ footerTd: 'border-r border-border last:border-r-0',
128
+ },
129
+ },
130
+ density: {
131
+ comfortable: {
132
+ th: 'px-4 py-3',
133
+ td: 'px-4 py-3',
134
+ footerTd: 'px-4 py-3',
135
+ },
136
+ compact: {
137
+ th: 'px-3 py-1.5',
138
+ td: 'px-3 py-1.5',
139
+ footerTd: 'px-3 py-1.5',
140
+ },
141
+ },
142
+ size: {
143
+ xs: { table: 'text-xs' },
144
+ sm: { table: 'text-sm' },
145
+ md: { table: 'text-sm' },
146
+ lg: { table: 'text-base' },
147
+ xl: { table: 'text-base' },
148
+ },
149
+ layout: {
150
+ auto: { table: 'table-auto' },
151
+ fixed: { table: 'table-fixed' },
152
+ },
153
+ stickyHeader: {
154
+ // Apply `position: sticky` to `<thead>` directly in addition to CDK's
155
+ // per-cell sticky. Group-level sticky on row groups is well supported
156
+ // across Chrome/Firefox/Safari and keeps the header visible reliably
157
+ // regardless of table layout (`border-separate` vs `border-collapse`)
158
+ // or consumer-supplied scroll ancestors. Cells still get their own bg
159
+ // so they stay opaque over scrolled rows.
160
+ true: {
161
+ table: '[&>thead]:sticky [&>thead]:top-0 [&>thead]:z-20 [&>thead>tr>th]:bg-surface-raised [&>thead>tr>th]:z-10',
162
+ },
163
+ false: {},
164
+ },
165
+ stickyFooter: {
166
+ true: {
167
+ table: '[&>tfoot]:sticky [&>tfoot]:bottom-0 [&>tfoot]:z-20 [&>tfoot>tr>td]:bg-surface-raised [&>tfoot>tr>td]:z-10',
168
+ },
169
+ false: {},
170
+ },
171
+ loading: {
172
+ true: { table: '[&>tbody]:opacity-60 [&>tbody]:pointer-events-none' },
173
+ false: {},
174
+ },
175
+ },
176
+ compoundVariants: [
177
+ {
178
+ variant: 'bordered',
179
+ density: 'compact',
180
+ class: { th: 'text-xs', td: 'text-xs', footerTd: 'text-xs' },
181
+ },
182
+ {
183
+ stickyHeader: true,
184
+ variant: 'striped',
185
+ class: {
186
+ table: '[&>thead>tr>th]:shadow-table-sticky',
187
+ },
188
+ },
189
+ ],
190
+ defaultVariants: {
191
+ variant: 'default',
192
+ density: 'comfortable',
193
+ size: 'md',
194
+ layout: 'auto',
195
+ stickyHeader: false,
196
+ stickyFooter: false,
197
+ loading: false,
198
+ },
199
+ }, { twMerge: true });
200
+ // ── Static class maps (Tailwind v4 JIT requires literal class strings) ──
201
+ const ALIGN_CLASSES = {
202
+ start: 'text-start',
203
+ center: 'text-center',
204
+ end: 'text-end',
205
+ };
206
+ const HIDE_BELOW_UTILITIES = {
207
+ sm: 'max-sm:hidden',
208
+ md: 'max-md:hidden',
209
+ lg: 'max-lg:hidden',
210
+ xl: 'max-xl:hidden',
211
+ };
212
+ /**
213
+ * Stack mode: CSS-only transform that makes each data row read like a labelled card
214
+ * below the chosen breakpoint. Applied to `<table>` via arbitrary descendant selectors
215
+ * because CDK owns the `<thead>` / `<tbody>` / `<tr>` elements.
216
+ *
217
+ * Every class is written in full so Tailwind v4's JIT scanner picks it up.
218
+ */
219
+ const STACK_TABLE_UTILITIES = {
220
+ sm: '[&>thead]:max-sm:hidden [&>tbody>tr]:max-sm:block [&>tbody>tr]:max-sm:border [&>tbody>tr]:max-sm:border-border [&>tbody>tr]:max-sm:rounded-lg [&>tbody>tr]:max-sm:p-3 [&>tbody>tr]:max-sm:mb-2 [&>tbody>tr]:max-sm:bg-surface-raised',
221
+ md: '[&>thead]:max-md:hidden [&>tbody>tr]:max-md:block [&>tbody>tr]:max-md:border [&>tbody>tr]:max-md:border-border [&>tbody>tr]:max-md:rounded-lg [&>tbody>tr]:max-md:p-3 [&>tbody>tr]:max-md:mb-2 [&>tbody>tr]:max-md:bg-surface-raised',
222
+ lg: '[&>thead]:max-lg:hidden [&>tbody>tr]:max-lg:block [&>tbody>tr]:max-lg:border [&>tbody>tr]:max-lg:border-border [&>tbody>tr]:max-lg:rounded-lg [&>tbody>tr]:max-lg:p-3 [&>tbody>tr]:max-lg:mb-2 [&>tbody>tr]:max-lg:bg-surface-raised',
223
+ xl: '[&>thead]:max-xl:hidden [&>tbody>tr]:max-xl:block [&>tbody>tr]:max-xl:border [&>tbody>tr]:max-xl:border-border [&>tbody>tr]:max-xl:rounded-lg [&>tbody>tr]:max-xl:p-3 [&>tbody>tr]:max-xl:mb-2 [&>tbody>tr]:max-xl:bg-surface-raised',
224
+ };
225
+ /** Stack mode per-cell decoration (applied to `<td>` directly via column classes). */
226
+ const STACK_CELL_UTILITIES = {
227
+ sm: 'max-sm:flex max-sm:justify-between max-sm:gap-3 max-sm:py-1 max-sm:border-0 max-sm:before:content-[attr(data-label)] max-sm:before:font-semibold max-sm:before:text-fg-muted max-sm:before:mr-4',
228
+ md: 'max-md:flex max-md:justify-between max-md:gap-3 max-md:py-1 max-md:border-0 max-md:before:content-[attr(data-label)] max-md:before:font-semibold max-md:before:text-fg-muted max-md:before:mr-4',
229
+ lg: 'max-lg:flex max-lg:justify-between max-lg:gap-3 max-lg:py-1 max-lg:border-0 max-lg:before:content-[attr(data-label)] max-lg:before:font-semibold max-lg:before:text-fg-muted max-lg:before:mr-4',
230
+ xl: 'max-xl:flex max-xl:justify-between max-xl:gap-3 max-xl:py-1 max-xl:border-0 max-xl:before:content-[attr(data-label)] max-xl:before:font-semibold max-xl:before:text-fg-muted max-xl:before:mr-4',
231
+ };
232
+ // CDK handles `position: sticky` + left/right offsets via its sticky styler.
233
+ // We only add the decorative background + z-index so overlapping cells stay opaque,
234
+ // plus a hairline shadow on the inner edge so the pinned column reads as floating
235
+ // over scrolled content. Direction follows the pinned edge:
236
+ // sticky-start (pinned left) → 1px shadow on the right side
237
+ // sticky-end (pinned right) → 1px shadow on the left side
238
+ const STICKY_CELL_ZINDEX = 'z-[5]';
239
+ const STICKY_START_SHADOW = 'shadow-table-sticky-cell-start';
240
+ const STICKY_END_SHADOW = 'shadow-table-sticky-cell-end';
241
+ const INTERACTIVE_TAGS = new Set([
242
+ 'BUTTON',
243
+ 'A',
244
+ 'INPUT',
245
+ 'SELECT',
246
+ 'OPTION',
247
+ 'TEXTAREA',
248
+ 'LABEL',
249
+ 'SUMMARY',
250
+ ]);
251
+ // ── Helpers ──────────────────────────────────────────────────────────
252
+ function formatLabel(template, vars) {
253
+ return template.replace(/\{(\w+)\}/g, (match, key) => Object.prototype.hasOwnProperty.call(vars, key) ? String(vars[key]) : match);
254
+ }
255
+ function coerceCssSize(value) {
256
+ if (value === null || value === undefined || value === '')
257
+ return null;
258
+ return typeof value === 'number' ? `${value}px` : value;
259
+ }
260
+ // ── Cell/Header/Footer template directives ────────────────────────────
261
+ //
262
+ // These DO NOT extend the CDK counterparts. The ColumnComponent renders its
263
+ // own <td cdk-cell *cdkCellDef>…</td> internally and forwards the consumer's
264
+ // TemplateRef via ngTemplateOutlet with a typed context. Decoupling from CDK
265
+ // here keeps our cell contexts strongly typed with extra fields (`columnIndex`,
266
+ // `column`, `row`) that CDK's `CdkCellOutletRowContext` lacks.
267
+ /** Structural directive on an `<ng-template>` (or a star-directive host) defining a column's data-cell template. Typed as `TwCellContext<T>`. */
268
+ class CellDefDirective {
269
+ /** @internal */
270
+ template = inject((TemplateRef));
271
+ /** @internal */
272
+ static ngTemplateContextGuard(_dir, _ctx) {
273
+ return true;
274
+ }
275
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: CellDefDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
276
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.14", type: CellDefDirective, isStandalone: true, selector: "[twCellDef]", ngImport: i0 });
277
+ }
278
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: CellDefDirective, decorators: [{
279
+ type: Directive,
280
+ args: [{ selector: '[twCellDef]' }]
281
+ }] });
282
+ /** Structural directive defining a column's header-cell template. Typed as `TwHeaderCellContext`. */
283
+ class HeaderCellDefDirective {
284
+ /** @internal */
285
+ template = inject((TemplateRef));
286
+ /** @internal */
287
+ static ngTemplateContextGuard(_dir, _ctx) {
288
+ return true;
289
+ }
290
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: HeaderCellDefDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
291
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.14", type: HeaderCellDefDirective, isStandalone: true, selector: "[twHeaderCellDef]", ngImport: i0 });
292
+ }
293
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: HeaderCellDefDirective, decorators: [{
294
+ type: Directive,
295
+ args: [{ selector: '[twHeaderCellDef]' }]
296
+ }] });
297
+ /** Structural directive defining a column's footer-cell template. Typed as `TwFooterCellContext<T>`. */
298
+ class FooterCellDefDirective {
299
+ /** @internal */
300
+ template = inject((TemplateRef));
301
+ /** @internal */
302
+ static ngTemplateContextGuard(_dir, _ctx) {
303
+ return true;
304
+ }
305
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: FooterCellDefDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
306
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.14", type: FooterCellDefDirective, isStandalone: true, selector: "[twFooterCellDef]", ngImport: i0 });
307
+ }
308
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: FooterCellDefDirective, decorators: [{
309
+ type: Directive,
310
+ args: [{ selector: '[twFooterCellDef]' }]
311
+ }] });
312
+ // ── No-data row + expansion directives ───────────────────────────────
313
+ //
314
+ // Row / header-row / footer-row defs are auto-generated inline in the
315
+ // `<tw-table>` template based on `<tw-column>` declarations. `*twNoDataRow`
316
+ // and `*twRowExpansion` remain as consumer-facing directives because they
317
+ // carry custom templates the consumer projects.
318
+ /** Structural directive on an `<ng-template>` declaring the no-data row fallback. Takes precedence over `[slot="empty"]` when both are present. */
319
+ class NoDataRowDirective extends CdkNoDataRow {
320
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: NoDataRowDirective, deps: null, target: i0.ɵɵFactoryTarget.Directive });
321
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.14", type: NoDataRowDirective, isStandalone: true, selector: "ng-template[twNoDataRow]", providers: [{ provide: CdkNoDataRow, useExisting: NoDataRowDirective }], usesInheritance: true, ngImport: i0 });
322
+ }
323
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: NoDataRowDirective, decorators: [{
324
+ type: Directive,
325
+ args: [{
326
+ selector: 'ng-template[twNoDataRow]',
327
+ providers: [{ provide: CdkNoDataRow, useExisting: NoDataRowDirective }],
328
+ }]
329
+ }] });
330
+ /** Structural directive on an `<ng-template>` declaring the row-expansion template. Requires `[multiTemplateRows]="true"` on the parent `<tw-table>`. */
331
+ class RowExpansionDirective {
332
+ /** Optional predicate that decides whether this expansion renders for a given row. Defaults to `undefined` (render for every expanded row). */
333
+ predicate = input(undefined, { ...(ngDevMode ? { debugName: "predicate" } : /* istanbul ignore next */ {}), alias: 'twRowExpansionWhen' });
334
+ /** @internal */
335
+ template = inject((TemplateRef));
336
+ /** @internal */
337
+ static ngTemplateContextGuard(_dir, _ctx) {
338
+ return true;
339
+ }
340
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: RowExpansionDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
341
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.14", type: RowExpansionDirective, isStandalone: true, selector: "ng-template[twRowExpansion]", inputs: { predicate: { classPropertyName: "predicate", publicName: "twRowExpansionWhen", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 });
342
+ }
343
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: RowExpansionDirective, decorators: [{
344
+ type: Directive,
345
+ args: [{ selector: 'ng-template[twRowExpansion]' }]
346
+ }], propDecorators: { predicate: [{ type: i0.Input, args: [{ isSignal: true, alias: "twRowExpansionWhen", required: false }] }] } });
347
+ // ── ColumnComponent ───────────────────────────────────────────────────
348
+ //
349
+ // `<tw-column>` is a pure metadata component — it holds the column's inputs
350
+ // and collects the consumer's header/data/footer cell templates via
351
+ // `contentChild`. The actual `<ng-container cdkColumnDef>` + cell renderers
352
+ // are emitted inline in `<tw-table>`'s template via a `@for` loop. This keeps
353
+ // every CDK defn (column, row, header-row, footer-row) inside CdkTable's
354
+ // DIRECT content, so its `@ContentChildren` queries resolve them on first
355
+ // render without our having to wire programmatic `addColumnDef` + deal with
356
+ // its timing quirks.
357
+ let nextColumnId = 0;
358
+ /**
359
+ * Declares a single column of a `<tw-table>`. Pure-metadata component — its
360
+ * host element is `display: none`; the column's `<th>` / `<td>` / `<tfoot>`
361
+ * renderers are emitted by the parent `<tw-table>` using the projected cell
362
+ * templates (`*twHeaderCellDef`, `*twCellDef`, `*twFooterCellDef`).
363
+ */
364
+ class ColumnComponent {
365
+ /** Unique identifier for this column. Required — referenced by `*twRowDef` / `*twHeaderRowDef` and used as the CDK `cdkColumnDef` name. */
366
+ name = input.required(...(ngDevMode ? [{ debugName: "name" }] : /* istanbul ignore next */ []));
367
+ /** Visual configuration: sticky, align, numeric, hideBelow, width. Accepts any subset; unset keys fall back to the defaults. */
368
+ display = input({}, ...(ngDevMode ? [{ debugName: "display" }] : /* istanbul ignore next */ []));
369
+ /** When true, removes this column from the visible column set. Defaults to `false`. */
370
+ hidden = input(false, ...(ngDevMode ? [{ debugName: "hidden" }] : /* istanbul ignore next */ []));
371
+ /** Ordering hint for the default visible-columns list when no `*twRowDef` is declared. Lower renders first. Defaults to `0`. */
372
+ priority = input(0, ...(ngDevMode ? [{ debugName: "priority" }] : /* istanbul ignore next */ []));
373
+ /** Plain text header label. Used when no `*twHeaderCellDef` template is projected. Defaults to `undefined`. */
374
+ headerLabel = input(undefined, ...(ngDevMode ? [{ debugName: "headerLabel" }] : /* istanbul ignore next */ []));
375
+ /** Label used as `data-label` on cells in responsive `'stack'` mode. Falls back to `headerLabel`, then `name`. Defaults to `undefined`. */
376
+ stackLabel = input(undefined, ...(ngDevMode ? [{ debugName: "stackLabel" }] : /* istanbul ignore next */ []));
377
+ /**
378
+ * Explicit override for the column header's `aria-sort` attribute. When unset (the default),
379
+ * the column derives its `aria-sort` from a parent `[twSort]` directive via the `TW_SORT_HANDLE`
380
+ * token — the column is treated as active when the directive's `active` id matches this column's
381
+ * `name`. Set explicitly to `'ascending'` / `'descending'` / `'none'` to override; `null` disables
382
+ * the attribute entirely. Defaults to `null`.
383
+ */
384
+ sortState = input(null, ...(ngDevMode ? [{ debugName: "sortState" }] : /* istanbul ignore next */ []));
385
+ /** @internal Stable identifier used for DOM `data-*` hooks. */
386
+ columnId = `tw-column-${nextColumnId++}`;
387
+ sortHandle = inject(TW_SORT_HANDLE, { optional: true });
388
+ /** @internal Consumer-projected header-cell template (optional). */
389
+ headerCellDef = contentChild(HeaderCellDefDirective, ...(ngDevMode ? [{ debugName: "headerCellDef" }] : /* istanbul ignore next */ []));
390
+ /** @internal Consumer-projected data-cell template. */
391
+ cellDef = contentChild((CellDefDirective), ...(ngDevMode ? [{ debugName: "cellDef" }] : /* istanbul ignore next */ []));
392
+ /** @internal Consumer-projected footer-cell template (optional). */
393
+ footerCellDef = contentChild((FooterCellDefDirective), ...(ngDevMode ? [{ debugName: "footerCellDef" }] : /* istanbul ignore next */ []));
394
+ /** @internal Index in the visible column set — pushed by the parent table. */
395
+ columnIndex = signal(0, ...(ngDevMode ? [{ debugName: "columnIndex" }] : /* istanbul ignore next */ []));
396
+ /** @internal Row snapshot pushed by the parent for footer-cell contexts. */
397
+ rowsSnapshot = signal([], ...(ngDevMode ? [{ debugName: "rowsSnapshot" }] : /* istanbul ignore next */ []));
398
+ /** @internal Table-pushed extra classes (sticky / hide-below / responsive decorations). */
399
+ extraCellClass = signal('', ...(ngDevMode ? [{ debugName: "extraCellClass" }] : /* istanbul ignore next */ []));
400
+ extraHeaderClass = signal('', ...(ngDevMode ? [{ debugName: "extraHeaderClass" }] : /* istanbul ignore next */ []));
401
+ extraFooterClass = signal('', ...(ngDevMode ? [{ debugName: "extraFooterClass" }] : /* istanbul ignore next */ []));
402
+ /** Resolved display config — partial input merged with `DISPLAY_DEFAULTS`. */
403
+ resolvedDisplay = computed(() => ({
404
+ ...DISPLAY_DEFAULTS,
405
+ ...this.display(),
406
+ }), ...(ngDevMode ? [{ debugName: "resolvedDisplay" }] : /* istanbul ignore next */ []));
407
+ /** @internal Convenience accessor for the cdkColumnDef `[sticky]` binding. */
408
+ stickyStart = computed(() => this.resolvedDisplay().sticky === 'start', ...(ngDevMode ? [{ debugName: "stickyStart" }] : /* istanbul ignore next */ []));
409
+ /** @internal Convenience accessor for the cdkColumnDef `[stickyEnd]` binding. */
410
+ stickyEnd = computed(() => this.resolvedDisplay().sticky === 'end', ...(ngDevMode ? [{ debugName: "stickyEnd" }] : /* istanbul ignore next */ []));
411
+ /** @internal Effective align — `numeric` forces `'end'` unless `align` is explicitly set to a non-default value. */
412
+ effectiveAlign = computed(() => {
413
+ const display = this.resolvedDisplay();
414
+ if (display.align === 'start' && display.numeric)
415
+ return 'end';
416
+ return display.align;
417
+ }, ...(ngDevMode ? [{ debugName: "effectiveAlign" }] : /* istanbul ignore next */ []));
418
+ /** @internal CSS width string applied to cells. */
419
+ resolvedWidth = computed(() => coerceCssSize(this.resolvedDisplay().width), ...(ngDevMode ? [{ debugName: "resolvedWidth" }] : /* istanbul ignore next */ []));
420
+ /** @internal `data-label` emitted on cells in stack mode. */
421
+ stackDataLabel = computed(() => this.stackLabel() ?? this.headerLabel() ?? this.name(), ...(ngDevMode ? [{ debugName: "stackDataLabel" }] : /* istanbul ignore next */ []));
422
+ /** @internal Align class plus tabular-numerals when `numeric` is true — shared by data and footer cells. */
423
+ dataCellPrefix = computed(() => {
424
+ const align = ALIGN_CLASSES[this.effectiveAlign()];
425
+ const numeric = this.resolvedDisplay().numeric ? '[font-variant-numeric:tabular-nums]' : '';
426
+ return numeric ? `${align} ${numeric}` : align;
427
+ }, ...(ngDevMode ? [{ debugName: "dataCellPrefix" }] : /* istanbul ignore next */ []));
428
+ cellClasses = computed(() => [this.dataCellPrefix(), this.extraCellClass()].filter(Boolean).join(' '), ...(ngDevMode ? [{ debugName: "cellClasses" }] : /* istanbul ignore next */ []));
429
+ headerCellClasses = computed(() => [ALIGN_CLASSES[this.effectiveAlign()], this.extraHeaderClass()].filter(Boolean).join(' '), ...(ngDevMode ? [{ debugName: "headerCellClasses" }] : /* istanbul ignore next */ []));
430
+ footerCellClasses = computed(() => [this.dataCellPrefix(), this.extraFooterClass()].filter(Boolean).join(' '), ...(ngDevMode ? [{ debugName: "footerCellClasses" }] : /* istanbul ignore next */ []));
431
+ /**
432
+ * @internal `aria-sort` value for the column's `<th>`. Explicit `sortState` wins; otherwise the
433
+ * column auto-resolves against a parent `[twSort]` directive (`TW_SORT_HANDLE`). Returns `null`
434
+ * when no sort is wired or the column is not the active sort, so the attribute is omitted.
435
+ */
436
+ ariaSort = computed(() => {
437
+ const explicit = this.sortState();
438
+ if (explicit !== null)
439
+ return explicit;
440
+ const handle = this.sortHandle;
441
+ if (!handle)
442
+ return null;
443
+ if (handle.active() !== this.name())
444
+ return null;
445
+ const direction = handle.direction();
446
+ if (direction === 'asc')
447
+ return 'ascending';
448
+ if (direction === 'desc')
449
+ return 'descending';
450
+ return null;
451
+ }, ...(ngDevMode ? [{ debugName: "ariaSort" }] : /* istanbul ignore next */ []));
452
+ headerContext = computed(() => ({
453
+ $implicit: this.name(),
454
+ column: this.name(),
455
+ columnIndex: this.columnIndex(),
456
+ }), ...(ngDevMode ? [{ debugName: "headerContext" }] : /* istanbul ignore next */ []));
457
+ footerContext = computed(() => ({
458
+ $implicit: this.name(),
459
+ column: this.name(),
460
+ columnIndex: this.columnIndex(),
461
+ rows: this.rowsSnapshot(),
462
+ }), ...(ngDevMode ? [{ debugName: "footerContext" }] : /* istanbul ignore next */ []));
463
+ /** @internal Builds the per-cell template context from CDK-provided row variables. */
464
+ buildCellContext(row, index, count, first, last, even, odd) {
465
+ return {
466
+ $implicit: row,
467
+ row,
468
+ column: this.name(),
469
+ index,
470
+ columnIndex: this.columnIndex(),
471
+ first,
472
+ last,
473
+ even,
474
+ odd,
475
+ count,
476
+ };
477
+ }
478
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: ColumnComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
479
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.2.14", type: ColumnComponent, isStandalone: true, selector: "tw-column", inputs: { name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: true, transformFunction: null }, display: { classPropertyName: "display", publicName: "display", isSignal: true, isRequired: false, transformFunction: null }, hidden: { classPropertyName: "hidden", publicName: "hidden", isSignal: true, isRequired: false, transformFunction: null }, priority: { classPropertyName: "priority", publicName: "priority", isSignal: true, isRequired: false, transformFunction: null }, headerLabel: { classPropertyName: "headerLabel", publicName: "headerLabel", isSignal: true, isRequired: false, transformFunction: null }, stackLabel: { classPropertyName: "stackLabel", publicName: "stackLabel", isSignal: true, isRequired: false, transformFunction: null }, sortState: { classPropertyName: "sortState", publicName: "sortState", isSignal: true, isRequired: false, transformFunction: null } }, host: { styleAttribute: "display: none" }, queries: [{ propertyName: "headerCellDef", first: true, predicate: HeaderCellDefDirective, descendants: true, isSignal: true }, { propertyName: "cellDef", first: true, predicate: (CellDefDirective), descendants: true, isSignal: true }, { propertyName: "footerCellDef", first: true, predicate: (FooterCellDefDirective), descendants: true, isSignal: true }], ngImport: i0, template: '', isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
480
+ }
481
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: ColumnComponent, decorators: [{
482
+ type: Component,
483
+ args: [{
484
+ selector: 'tw-column',
485
+ changeDetection: ChangeDetectionStrategy.OnPush,
486
+ template: '',
487
+ host: {
488
+ 'style': 'display: none',
489
+ },
490
+ }]
491
+ }], propDecorators: { name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: true }] }], display: [{ type: i0.Input, args: [{ isSignal: true, alias: "display", required: false }] }], hidden: [{ type: i0.Input, args: [{ isSignal: true, alias: "hidden", required: false }] }], priority: [{ type: i0.Input, args: [{ isSignal: true, alias: "priority", required: false }] }], headerLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "headerLabel", required: false }] }], stackLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "stackLabel", required: false }] }], sortState: [{ type: i0.Input, args: [{ isSignal: true, alias: "sortState", required: false }] }], headerCellDef: [{ type: i0.ContentChild, args: [i0.forwardRef(() => HeaderCellDefDirective), { isSignal: true }] }], cellDef: [{ type: i0.ContentChild, args: [i0.forwardRef(() => CellDefDirective), { isSignal: true }] }], footerCellDef: [{ type: i0.ContentChild, args: [i0.forwardRef(() => FooterCellDefDirective), { isSignal: true }] }] } });
492
+ // ── TableComponent ────────────────────────────────────────────────────
493
+ let nextTableId = 0;
494
+ /**
495
+ * A highly customizable data-table component wrapping `@angular/cdk/table`.
496
+ *
497
+ * Composes with other ngx-tw primitives:
498
+ * - Sortable headers: place `[twSort]` on `<tw-table>` and `[tw-sort-header]` inside `*twHeaderCellDef`.
499
+ * - Pagination: project `<tw-paginator slot="pagination">` as a child.
500
+ * - Loading spinner: project `<tw-spinner slot="loading">` or rely on the built-in fallback.
501
+ */
502
+ class TableComponent {
503
+ // ── Inputs ──
504
+ /** The table's rows. Accepts a plain array, an `Observable<readonly T[]>`, or a CDK `DataSource<T>`. Wired directly to `CdkTable.dataSource`. Defaults to an empty array. */
505
+ data = input([], ...(ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []));
506
+ /** Tracking function used by CDK to identify rows across data changes. When unset, CDK uses identity. */
507
+ trackBy = input(undefined, ...(ngDevMode ? [{ debugName: "trackBy" }] : /* istanbul ignore next */ []));
508
+ /** When true, renders the loading slot as an overlay and applies `opacity-60 pointer-events-none` to the body. Defaults to `false`. */
509
+ loading = input(false, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
510
+ /** When non-null, renders the error slot in place of the body. Defaults to `null`. */
511
+ error = input(null, ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
512
+ /** Visual configuration — `variant`, `density`, `size`, `layout`, `rowAnimations`. Accepts a partial; unset keys fall back to the defaults. */
513
+ appearance = input({}, ...(ngDevMode ? [{ debugName: "appearance" }] : /* istanbul ignore next */ []));
514
+ /** Sticky configuration — `header`, `footer`, `scrollHeight`. Accepts a partial; unset keys fall back to the defaults. */
515
+ sticky = input({}, ...(ngDevMode ? [{ debugName: "sticky" }] : /* istanbul ignore next */ []));
516
+ /** Responsive configuration — `mode`, `stackBelow`. Accepts a partial; unset keys fall back to the defaults. */
517
+ responsive = input({}, ...(ngDevMode ? [{ debugName: "responsive" }] : /* istanbul ignore next */ []));
518
+ /** Selection configuration — `enabled`. Accepts a partial; unset keys fall back to the defaults. */
519
+ selection = input({}, ...(ngDevMode ? [{ debugName: "selection" }] : /* istanbul ignore next */ []));
520
+ /** Whether multiple row templates may render per data object. Required for `*twRowExpansion` and advanced `*twRowDef [when]` usage. Defaults to `false`. */
521
+ multiTemplateRows = input(false, ...(ngDevMode ? [{ debugName: "multiTemplateRows" }] : /* istanbul ignore next */ []));
522
+ /** Overrides for user-facing strings. Unset keys fall back to the English defaults. Defaults to `{}`. */
523
+ labels = input({}, ...(ngDevMode ? [{ debugName: "labels" }] : /* istanbul ignore next */ []));
524
+ /** Accessible name for the `<table>`. Required when no visible `<caption slot="caption">` is provided. */
525
+ ariaLabel = input(undefined, { ...(ngDevMode ? { debugName: "ariaLabel" } : /* istanbul ignore next */ {}), alias: 'aria-label' });
526
+ /** Id of an external element labelling the table. Mirrored to `aria-labelledby` on the `<table>`. */
527
+ ariaLabelledby = input(undefined, { ...(ngDevMode ? { debugName: "ariaLabelledby" } : /* istanbul ignore next */ {}), alias: 'aria-labelledby' });
528
+ // ── Models (two-way) ──
529
+ /** Two-way bound set of rows currently expanded. Immutable — set a new `Set` on every change; do not mutate in place. */
530
+ expandedRows = model(new Set(), ...(ngDevMode ? [{ debugName: "expandedRows" }] : /* istanbul ignore next */ []));
531
+ /** Two-way bound list of selected rows. Set a new array on every change; do not mutate. Only used when `selection.enabled` is `true`. */
532
+ selected = model([], ...(ngDevMode ? [{ debugName: "selected" }] : /* istanbul ignore next */ []));
533
+ // ── Outputs ──
534
+ /** Fires when a row is clicked. Suppressed when the click originated inside an interactive descendant (button, link, input, etc.). */
535
+ rowClicked = output();
536
+ /** Fires after `selected` changes via user interaction (not on programmatic writes). */
537
+ selectionChange = output();
538
+ /** Fires after a row is expanded or collapsed by user interaction. */
539
+ expansionChange = output();
540
+ // ── Content queries ──
541
+ /** @internal Projected columns. */
542
+ columns = contentChildren((ColumnComponent), { ...(ngDevMode ? { debugName: "columns" } : /* istanbul ignore next */ {}), descendants: true });
543
+ /** @internal Projected custom no-data row (takes precedence over the fallback empty state). */
544
+ noDataRow = contentChild(NoDataRowDirective, { ...(ngDevMode ? { debugName: "noDataRow" } : /* istanbul ignore next */ {}), descendants: true });
545
+ /** @internal Projected expansion row template. */
546
+ expansionTemplate = contentChild((RowExpansionDirective), { ...(ngDevMode ? { debugName: "expansionTemplate" } : /* istanbul ignore next */ {}), descendants: true });
547
+ // ── View children ──
548
+ /** @internal The CDK table inside our view. */
549
+ cdkTable = viewChild.required('cdkTable');
550
+ // ── Injected services ──
551
+ _liveAnnouncer = inject(LiveAnnouncer);
552
+ _elementRef = inject((ElementRef));
553
+ _destroyRef = inject(DestroyRef);
554
+ // ── Identifiers ──
555
+ /** Unique per-instance id used to scope row/expansion DOM ids. */
556
+ hostId = `tw-table-${nextTableId++}`;
557
+ /** Returns the DOM id used for the expansion row of the given data row at the given index. Useful for wiring `aria-controls` from a trigger cell. */
558
+ expansionId = (_row, index) => `${this.hostId}-expansion-${index}`;
559
+ // ── Resolved config (partial input merged with defaults) ──
560
+ /** @internal Resolved visual configuration. */
561
+ resolvedAppearance = computed(() => ({
562
+ ...APPEARANCE_DEFAULTS,
563
+ ...this.appearance(),
564
+ }), ...(ngDevMode ? [{ debugName: "resolvedAppearance" }] : /* istanbul ignore next */ []));
565
+ /** @internal Resolved sticky configuration. */
566
+ resolvedSticky = computed(() => ({
567
+ ...STICKY_DEFAULTS,
568
+ ...this.sticky(),
569
+ }), ...(ngDevMode ? [{ debugName: "resolvedSticky" }] : /* istanbul ignore next */ []));
570
+ /** @internal Resolved responsive configuration. */
571
+ resolvedResponsive = computed(() => ({
572
+ ...RESPONSIVE_DEFAULTS,
573
+ ...this.responsive(),
574
+ }), ...(ngDevMode ? [{ debugName: "resolvedResponsive" }] : /* istanbul ignore next */ []));
575
+ /** @internal Resolved selection configuration. */
576
+ resolvedSelection = computed(() => ({
577
+ ...SELECTION_DEFAULTS,
578
+ ...this.selection(),
579
+ }), ...(ngDevMode ? [{ debugName: "resolvedSelection" }] : /* istanbul ignore next */ []));
580
+ // ── Derived state ──
581
+ /** @internal Snapshot of the data array (for footer-cell context). Empty array for async data sources. */
582
+ resolvedRows = computed(() => {
583
+ const d = this.data();
584
+ return Array.isArray(d) ? d : [];
585
+ }, ...(ngDevMode ? [{ debugName: "resolvedRows" }] : /* istanbul ignore next */ []));
586
+ /** @internal Whether the data input is a plain array. */
587
+ dataIsArray = computed(() => Array.isArray(this.data()), ...(ngDevMode ? [{ debugName: "dataIsArray" }] : /* istanbul ignore next */ []));
588
+ /** @internal Snapshot-based empty check; `false` for async sources (we can't know without subscribing). */
589
+ isEmptySnapshot = computed(() => this.dataIsArray() && this.resolvedRows().length === 0, ...(ngDevMode ? [{ debugName: "isEmptySnapshot" }] : /* istanbul ignore next */ []));
590
+ /** @internal Whether the empty overlay fallback should render (no no-data-row projected). */
591
+ shouldRenderEmptyOverlay = computed(() => this.isEmptySnapshot() &&
592
+ !this.loading() &&
593
+ this.error() === null &&
594
+ !this.noDataRow(), ...(ngDevMode ? [{ debugName: "shouldRenderEmptyOverlay" }] : /* istanbul ignore next */ []));
595
+ /** @internal Whether the error overlay should render. */
596
+ shouldRenderErrorOverlay = computed(() => this.error() !== null, ...(ngDevMode ? [{ debugName: "shouldRenderErrorOverlay" }] : /* istanbul ignore next */ []));
597
+ /** @internal Resolved table labels (defaults merged with input overrides). */
598
+ resolvedLabels = computed(() => ({
599
+ ...DEFAULT_TABLE_LABELS,
600
+ ...this.labels(),
601
+ }), ...(ngDevMode ? [{ debugName: "resolvedLabels" }] : /* istanbul ignore next */ []));
602
+ /** @internal Computed error message for the default error overlay. Empty when no error is set. */
603
+ errorMessage = computed(() => {
604
+ const err = this.error();
605
+ if (err == null)
606
+ return '';
607
+ return `${this.resolvedLabels().errorPrefix}${String(err)}`;
608
+ }, ...(ngDevMode ? [{ debugName: "errorMessage" }] : /* istanbul ignore next */ []));
609
+ /** @internal CSS max-height string for the scroll container. */
610
+ maxHeightStyle = computed(() => coerceCssSize(this.resolvedSticky().scrollHeight), ...(ngDevMode ? [{ debugName: "maxHeightStyle" }] : /* istanbul ignore next */ []));
611
+ /** @internal The visible (non-hidden) columns in render order (by ascending `priority`). */
612
+ visibleColumns = computed(() => this.columns()
613
+ .filter((c) => !c.hidden())
614
+ .sort((a, b) => a.priority() - b.priority()), ...(ngDevMode ? [{ debugName: "visibleColumns" }] : /* istanbul ignore next */ []));
615
+ /** @internal Column names of the visible set, prefixed with `_selection` when `selection.enabled === true`. */
616
+ visibleColumnNames = computed(() => {
617
+ const names = this.visibleColumns().map((c) => c.name());
618
+ return this.resolvedSelection().enabled ? ['_selection', ...names] : names;
619
+ }, ...(ngDevMode ? [{ debugName: "visibleColumnNames" }] : /* istanbul ignore next */ []));
620
+ /** @internal Whether any column has a projected footer-cell template. */
621
+ hasFooterRow = computed(() => this.visibleColumns().some((c) => !!c.footerCellDef()), ...(ngDevMode ? [{ debugName: "hasFooterRow" }] : /* istanbul ignore next */ []));
622
+ /** @internal Whether an expansion template has been declared and multi-template rows are enabled. */
623
+ hasExpansion = computed(() => this.multiTemplateRows() && !!this.expansionTemplate(), ...(ngDevMode ? [{ debugName: "hasExpansion" }] : /* istanbul ignore next */ []));
624
+ /** @internal Colspan to use for the expansion `<td>`. */
625
+ expansionColspan = computed(() => {
626
+ const cols = this.visibleColumns().length;
627
+ return Math.max(1, cols + (this.resolvedSelection().enabled ? 1 : 0));
628
+ }, ...(ngDevMode ? [{ debugName: "expansionColspan" }] : /* istanbul ignore next */ []));
629
+ /** @internal Pulled by the template for the `*cdkHeaderRowDef`/`*cdkFooterRowDef` `sticky:` field. */
630
+ stickyHeader = computed(() => this.resolvedSticky().header, ...(ngDevMode ? [{ debugName: "stickyHeader" }] : /* istanbul ignore next */ []));
631
+ stickyFooter = computed(() => this.resolvedSticky().footer, ...(ngDevMode ? [{ debugName: "stickyFooter" }] : /* istanbul ignore next */ []));
632
+ // ── Variant class computation ──
633
+ _variantResult = computed(() => {
634
+ const appearance = this.resolvedAppearance();
635
+ const stickyCfg = this.resolvedSticky();
636
+ return tableVariants({
637
+ variant: appearance.variant,
638
+ density: appearance.density,
639
+ size: appearance.size,
640
+ layout: appearance.layout,
641
+ stickyHeader: stickyCfg.header,
642
+ stickyFooter: stickyCfg.footer,
643
+ loading: this.loading(),
644
+ });
645
+ }, ...(ngDevMode ? [{ debugName: "_variantResult" }] : /* istanbul ignore next */ []));
646
+ hostClasses = computed(() => this._variantResult().root(), ...(ngDevMode ? [{ debugName: "hostClasses" }] : /* istanbul ignore next */ []));
647
+ toolbarClasses = computed(() => this._variantResult().toolbar(), ...(ngDevMode ? [{ debugName: "toolbarClasses" }] : /* istanbul ignore next */ []));
648
+ scrollWrapperClasses = computed(() => this._variantResult().scrollWrapper(), ...(ngDevMode ? [{ debugName: "scrollWrapperClasses" }] : /* istanbul ignore next */ []));
649
+ scrollContainerClasses = computed(() => {
650
+ const base = this._variantResult().scrollContainer();
651
+ return this.maxHeightStyle() ? `${base} overflow-y-auto` : base;
652
+ }, ...(ngDevMode ? [{ debugName: "scrollContainerClasses" }] : /* istanbul ignore next */ []));
653
+ tableClasses = computed(() => {
654
+ const base = this._variantResult().table();
655
+ const responsive = this.resolvedResponsive();
656
+ if (responsive.mode !== 'stack')
657
+ return base;
658
+ return `${base} ${STACK_TABLE_UTILITIES[responsive.stackBelow]}`;
659
+ }, ...(ngDevMode ? [{ debugName: "tableClasses" }] : /* istanbul ignore next */ []));
660
+ thClasses = computed(() => this._variantResult().th(), ...(ngDevMode ? [{ debugName: "thClasses" }] : /* istanbul ignore next */ []));
661
+ tdClasses = computed(() => {
662
+ const base = this._variantResult().td();
663
+ const responsive = this.resolvedResponsive();
664
+ if (responsive.mode === 'stack') {
665
+ return `${base} ${STACK_CELL_UTILITIES[responsive.stackBelow]}`;
666
+ }
667
+ return base;
668
+ }, ...(ngDevMode ? [{ debugName: "tdClasses" }] : /* istanbul ignore next */ []));
669
+ footerTdClasses = computed(() => this._variantResult().footerTd(), ...(ngDevMode ? [{ debugName: "footerTdClasses" }] : /* istanbul ignore next */ []));
670
+ emptyStateClasses = computed(() => this._variantResult().emptyState(), ...(ngDevMode ? [{ debugName: "emptyStateClasses" }] : /* istanbul ignore next */ []));
671
+ emptyIconClasses = computed(() => this._variantResult().emptyIcon(), ...(ngDevMode ? [{ debugName: "emptyIconClasses" }] : /* istanbul ignore next */ []));
672
+ emptyMessageClasses = computed(() => this._variantResult().emptyMessage(), ...(ngDevMode ? [{ debugName: "emptyMessageClasses" }] : /* istanbul ignore next */ []));
673
+ loadingOverlayClasses = computed(() => this._variantResult().loadingOverlay(), ...(ngDevMode ? [{ debugName: "loadingOverlayClasses" }] : /* istanbul ignore next */ []));
674
+ loadingMessageClasses = computed(() => this._variantResult().loadingMessage(), ...(ngDevMode ? [{ debugName: "loadingMessageClasses" }] : /* istanbul ignore next */ []));
675
+ errorStateClasses = computed(() => this._variantResult().errorState(), ...(ngDevMode ? [{ debugName: "errorStateClasses" }] : /* istanbul ignore next */ []));
676
+ errorIconClasses = computed(() => this._variantResult().errorIcon(), ...(ngDevMode ? [{ debugName: "errorIconClasses" }] : /* istanbul ignore next */ []));
677
+ errorMessageClasses = computed(() => this._variantResult().errorMessage(), ...(ngDevMode ? [{ debugName: "errorMessageClasses" }] : /* istanbul ignore next */ []));
678
+ footerSlotClasses = computed(() => this._variantResult().footerSlot(), ...(ngDevMode ? [{ debugName: "footerSlotClasses" }] : /* istanbul ignore next */ []));
679
+ paginationSlotClasses = computed(() => this._variantResult().paginationSlot(), ...(ngDevMode ? [{ debugName: "paginationSlotClasses" }] : /* istanbul ignore next */ []));
680
+ expansionRowClasses = computed(() => this._variantResult().expansionRow(), ...(ngDevMode ? [{ debugName: "expansionRowClasses" }] : /* istanbul ignore next */ []));
681
+ expansionCellClasses = computed(() => this._variantResult().expansionCell(), ...(ngDevMode ? [{ debugName: "expansionCellClasses" }] : /* istanbul ignore next */ []));
682
+ selectionHeaderClasses = computed(() => {
683
+ const base = this._variantResult().selectionHeader();
684
+ return [base, this.thClasses()].filter(Boolean).join(' ');
685
+ }, ...(ngDevMode ? [{ debugName: "selectionHeaderClasses" }] : /* istanbul ignore next */ []));
686
+ selectionCellClasses = computed(() => {
687
+ const base = this._variantResult().selectionCell();
688
+ return [base, this.tdClasses()].filter(Boolean).join(' ');
689
+ }, ...(ngDevMode ? [{ debugName: "selectionCellClasses" }] : /* istanbul ignore next */ []));
690
+ // ── CdkTable wiring ──
691
+ //
692
+ // All CdkColumnDefs + row defs are rendered INLINE in our template (see
693
+ // `table.html`), so CdkTable's own @ContentChildren queries resolve them on
694
+ // first render without our having to programmatically register anything.
695
+ //
696
+ // The noDataRow and expansion template are projected by consumers — we bridge
697
+ // those to CdkTable via `setNoDataRow` (expansion uses an inline fallback
698
+ // column + row def in the template).
699
+ constructor() {
700
+ // Forward dataSource / trackBy / multiTemplateRows / fixedLayout to the CDK
701
+ // table whenever the inputs change.
702
+ effect(() => {
703
+ const table = this.cdkTable();
704
+ if (!table)
705
+ return;
706
+ table.dataSource = this.data();
707
+ const tb = this.trackBy();
708
+ if (tb)
709
+ table.trackBy = tb;
710
+ table.multiTemplateDataRows = this.multiTemplateRows();
711
+ table.fixedLayout = this.resolvedAppearance().layout === 'fixed';
712
+ });
713
+ // Bridge the projected no-data row to CdkTable.
714
+ let registeredNoDataRow = null;
715
+ effect(() => {
716
+ const table = this.cdkTable();
717
+ const noDataRow = this.noDataRow() ?? null;
718
+ if (!table)
719
+ return;
720
+ if (registeredNoDataRow !== noDataRow) {
721
+ table.setNoDataRow(noDataRow);
722
+ registeredNoDataRow = noDataRow;
723
+ }
724
+ });
725
+ // Expansion-set changes don't mutate `data`, so CdkTable's internal differ
726
+ // doesn't know to re-evaluate row `when` predicates. Trigger a manual render
727
+ // whenever the expansion set changes.
728
+ effect(() => {
729
+ this.expandedRows();
730
+ const table = untracked(() => this.cdkTable());
731
+ if (table)
732
+ queueMicrotask(() => table.renderRows());
733
+ });
734
+ // Update each column's extra-class signals + columnIndex on relevant input changes.
735
+ // Pushes the table-level `th` / `td` / `footerTd` classes plus per-column decorations
736
+ // (sticky, hideBelow). ColumnComponent combines these with its own align/numeric bits.
737
+ effect(() => {
738
+ const visible = this.visibleColumns();
739
+ const responsiveMode = this.resolvedResponsive().mode;
740
+ const thCls = this.thClasses();
741
+ const tdCls = this.tdClasses();
742
+ const footerCls = this.footerTdClasses();
743
+ untracked(() => {
744
+ const allCols = this.columns();
745
+ allCols.forEach((col) => {
746
+ const visibleIndex = visible.indexOf(col);
747
+ col.columnIndex.set(Math.max(0, visibleIndex));
748
+ const display = col.resolvedDisplay();
749
+ const decor = [];
750
+ if (display.sticky === 'start') {
751
+ decor.push(STICKY_CELL_ZINDEX, 'bg-surface-raised', STICKY_START_SHADOW);
752
+ }
753
+ else if (display.sticky === 'end') {
754
+ decor.push(STICKY_CELL_ZINDEX, 'bg-surface-raised', STICKY_END_SHADOW);
755
+ }
756
+ if (display.hideBelow && responsiveMode === 'hide') {
757
+ decor.push(HIDE_BELOW_UTILITIES[display.hideBelow]);
758
+ }
759
+ const decorCls = decor.join(' ');
760
+ col.extraHeaderClass.set(`${thCls} ${decorCls}`.trim());
761
+ col.extraCellClass.set(`${tdCls} ${decorCls}`.trim());
762
+ col.extraFooterClass.set(`${footerCls} ${decorCls}`.trim());
763
+ });
764
+ });
765
+ });
766
+ // Push row snapshot into each column for footer-cell contexts.
767
+ effect(() => {
768
+ const rows = this.resolvedRows();
769
+ const cols = this.columns();
770
+ untracked(() => cols.forEach((c) => c.rowsSnapshot.set(rows)));
771
+ });
772
+ // Dev-mode: duplicate column name guard.
773
+ effect(() => {
774
+ if (!isDevMode())
775
+ return;
776
+ const names = this.columns().map((c) => c.name());
777
+ const seen = new Set();
778
+ for (const n of names) {
779
+ if (seen.has(n)) {
780
+ throw new Error(`[tw-table] duplicate column name '${n}'. Every <tw-column> must have a unique \`name\`.`);
781
+ }
782
+ seen.add(n);
783
+ }
784
+ });
785
+ // Dev-mode: require multiTemplateRows when row-expansion is declared.
786
+ effect(() => {
787
+ if (!isDevMode())
788
+ return;
789
+ const tpl = this.expansionTemplate();
790
+ if (tpl && !this.multiTemplateRows()) {
791
+ throw new Error('[tw-table] *twRowExpansion requires [multiTemplateRows]="true" on <tw-table>.');
792
+ }
793
+ });
794
+ // Dev-mode: accessible-name warning, once mounted.
795
+ afterNextRender(() => {
796
+ if (!isDevMode())
797
+ return;
798
+ const hasCaption = !!this._elementRef.nativeElement.querySelector('caption');
799
+ if (!hasCaption && !this.ariaLabel() && !this.ariaLabelledby()) {
800
+ console.warn('[tw-table] no accessible name provided. Add a `<caption slot="caption">`, `[aria-label]`, or `[aria-labelledby]`.');
801
+ }
802
+ });
803
+ // Loading announcement.
804
+ //
805
+ // The effect tracks `this.loading()` only; reads of labels / rows are wrapped
806
+ // in `untracked()` so unrelated upstream signals (label updates, row mutations
807
+ // that don't toggle loading) don't re-announce. The announcement should fire
808
+ // once per loading-state transition, never on incidental data changes.
809
+ effect(() => {
810
+ const loading = this.loading();
811
+ const labels = untracked(() => this.resolvedLabels());
812
+ if (loading) {
813
+ this._liveAnnouncer.announce(labels.loading, 'polite');
814
+ }
815
+ else {
816
+ const count = untracked(() => this.dataIsArray() ? this.resolvedRows().length : 0);
817
+ if (count > 0) {
818
+ this._liveAnnouncer.announce(formatLabel(labels.rowsUpdatedAnnouncement, { count }), 'polite');
819
+ }
820
+ }
821
+ });
822
+ }
823
+ // ── Row-click + expansion + selection helpers ──
824
+ /** @internal Called by the template when a data row is clicked. */
825
+ handleRowClick(row, index, event) {
826
+ const path = typeof event.composedPath === 'function' ? event.composedPath() : [];
827
+ for (const node of path) {
828
+ if (!(node instanceof HTMLElement))
829
+ continue;
830
+ if (node === event.currentTarget)
831
+ break;
832
+ if (INTERACTIVE_TAGS.has(node.tagName))
833
+ return;
834
+ if (node.hasAttribute('tabindex') && node.tabIndex >= 0 && node.tagName !== 'TR')
835
+ return;
836
+ const role = node.getAttribute('role');
837
+ if (role === 'button' || role === 'link' || role === 'checkbox')
838
+ return;
839
+ }
840
+ this.rowClicked.emit({ row, index, event });
841
+ }
842
+ /** @internal Whether a row is currently expanded. */
843
+ isRowExpanded(row) {
844
+ return this.expandedRows().has(row);
845
+ }
846
+ /** @internal CDK `when` predicate used for the auto-generated expansion row. */
847
+ expansionWhen = (index, row) => {
848
+ if (!this.hasExpansion())
849
+ return false;
850
+ if (!this.expandedRows().has(row))
851
+ return false;
852
+ const pred = this.expansionTemplate()?.predicate();
853
+ if (typeof pred === 'function' && !pred(row, index))
854
+ return false;
855
+ return true;
856
+ };
857
+ /** @internal Context object for the expansion template. */
858
+ expansionContext(row, index) {
859
+ return {
860
+ $implicit: row,
861
+ row,
862
+ index,
863
+ collapse: () => this.collapse(row),
864
+ };
865
+ }
866
+ /** Expands the given row if not already expanded and emits `expansionChange`. */
867
+ expand(row) {
868
+ if (this.isRowExpanded(row))
869
+ return;
870
+ const next = new Set(this.expandedRows());
871
+ next.add(row);
872
+ this.expandedRows.set(next);
873
+ this.expansionChange.emit({ row, expanded: true, expandedRows: next });
874
+ }
875
+ /** Collapses the given row if currently expanded and emits `expansionChange`. */
876
+ collapse(row) {
877
+ if (!this.isRowExpanded(row))
878
+ return;
879
+ const next = new Set(this.expandedRows());
880
+ next.delete(row);
881
+ this.expandedRows.set(next);
882
+ this.expansionChange.emit({ row, expanded: false, expandedRows: next });
883
+ }
884
+ /** Toggles the expansion state of the given row. */
885
+ toggleExpansion(row) {
886
+ if (this.isRowExpanded(row))
887
+ this.collapse(row);
888
+ else
889
+ this.expand(row);
890
+ }
891
+ /** Whether a given row is currently in the `selected` list. */
892
+ isSelected(row) {
893
+ return this.selected().includes(row);
894
+ }
895
+ /** Selects or deselects a row and emits `selectionChange`. */
896
+ setSelected(row, nextSelected) {
897
+ const previous = this.selected();
898
+ const isAlreadySelected = previous.includes(row);
899
+ if (isAlreadySelected === nextSelected)
900
+ return;
901
+ const next = nextSelected ? [...previous, row] : previous.filter((r) => r !== row);
902
+ this.selected.set(next);
903
+ this.selectionChange.emit({
904
+ selected: next,
905
+ added: nextSelected ? [row] : [],
906
+ removed: nextSelected ? [] : [row],
907
+ previous,
908
+ });
909
+ }
910
+ /**
911
+ * Adds every row in the current data snapshot to `selected` that isn't already selected.
912
+ * Emits `selectionChange` if anything changed. Only the array data-source path can compute a
913
+ * full snapshot — for `Observable<T[]>` or `DataSource<T>` data inputs this is a no-op.
914
+ */
915
+ selectAll() {
916
+ const rows = this.resolvedRows();
917
+ if (rows.length === 0)
918
+ return;
919
+ const previous = this.selected();
920
+ const previousSet = new Set(previous);
921
+ const added = [];
922
+ for (const row of rows) {
923
+ if (!previousSet.has(row))
924
+ added.push(row);
925
+ }
926
+ if (added.length === 0)
927
+ return;
928
+ const next = [...previous, ...added];
929
+ this.selected.set(next);
930
+ this.selectionChange.emit({ selected: next, added, removed: [], previous });
931
+ }
932
+ /** Clears every currently-selected row. Emits `selectionChange` if anything was selected. */
933
+ clearSelection() {
934
+ const previous = this.selected();
935
+ if (previous.length === 0)
936
+ return;
937
+ this.selected.set([]);
938
+ this.selectionChange.emit({ selected: [], added: [], removed: previous, previous });
939
+ }
940
+ /** @internal Returns `aria-selected` for a data row, or `null` to omit the attribute when selection is disabled. */
941
+ rowAriaSelected(row) {
942
+ if (!this.resolvedSelection().enabled)
943
+ return null;
944
+ return this.isSelected(row) ? 'true' : 'false';
945
+ }
946
+ /**
947
+ * @internal Tri-state summary of the current selection vs. the visible data snapshot.
948
+ * `'none'` — nothing selected; `'some'` — partial; `'all'` — every snapshot row is selected.
949
+ */
950
+ masterSelectionState = computed(() => {
951
+ const rows = this.resolvedRows();
952
+ if (rows.length === 0)
953
+ return 'none';
954
+ const selected = this.selected();
955
+ if (selected.length === 0)
956
+ return 'none';
957
+ const selectedSet = new Set(selected);
958
+ for (const row of rows) {
959
+ if (!selectedSet.has(row))
960
+ return 'some';
961
+ }
962
+ return 'all';
963
+ }, ...(ngDevMode ? [{ debugName: "masterSelectionState" }] : /* istanbul ignore next */ []));
964
+ /** @internal Whether the master "select all" checkbox should render checked. */
965
+ masterChecked = computed(() => this.masterSelectionState() === 'all', ...(ngDevMode ? [{ debugName: "masterChecked" }] : /* istanbul ignore next */ []));
966
+ /** @internal Whether the master "select all" checkbox should render indeterminate. */
967
+ masterIndeterminate = computed(() => this.masterSelectionState() === 'some', ...(ngDevMode ? [{ debugName: "masterIndeterminate" }] : /* istanbul ignore next */ []));
968
+ /** @internal Wires the master checkbox change event to `selectAll` / `clearSelection`. */
969
+ toggleMasterSelection(checked) {
970
+ if (checked)
971
+ this.selectAll();
972
+ else
973
+ this.clearSelection();
974
+ }
975
+ /** @internal Formatted accessible label for a per-row selection checkbox. */
976
+ selectRowLabel(rowIndex) {
977
+ return formatLabel(this.resolvedLabels().selectRowLabel, { index: rowIndex + 1 });
978
+ }
979
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: TableComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
980
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: TableComponent, isStandalone: true, selector: "tw-table", inputs: { data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: false, transformFunction: null }, trackBy: { classPropertyName: "trackBy", publicName: "trackBy", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, error: { classPropertyName: "error", publicName: "error", isSignal: true, isRequired: false, transformFunction: null }, appearance: { classPropertyName: "appearance", publicName: "appearance", isSignal: true, isRequired: false, transformFunction: null }, sticky: { classPropertyName: "sticky", publicName: "sticky", isSignal: true, isRequired: false, transformFunction: null }, responsive: { classPropertyName: "responsive", publicName: "responsive", isSignal: true, isRequired: false, transformFunction: null }, selection: { classPropertyName: "selection", publicName: "selection", isSignal: true, isRequired: false, transformFunction: null }, multiTemplateRows: { classPropertyName: "multiTemplateRows", publicName: "multiTemplateRows", isSignal: true, isRequired: false, transformFunction: null }, labels: { classPropertyName: "labels", publicName: "labels", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "aria-label", isSignal: true, isRequired: false, transformFunction: null }, ariaLabelledby: { classPropertyName: "ariaLabelledby", publicName: "aria-labelledby", isSignal: true, isRequired: false, transformFunction: null }, expandedRows: { classPropertyName: "expandedRows", publicName: "expandedRows", isSignal: true, isRequired: false, transformFunction: null }, selected: { classPropertyName: "selected", publicName: "selected", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { expandedRows: "expandedRowsChange", selected: "selectedChange", rowClicked: "rowClicked", selectionChange: "selectionChange", expansionChange: "expansionChange" }, host: { properties: { "class": "hostClasses()" } }, queries: [{ propertyName: "columns", predicate: (ColumnComponent), descendants: true, isSignal: true }, { propertyName: "noDataRow", first: true, predicate: NoDataRowDirective, descendants: true, isSignal: true }, { propertyName: "expansionTemplate", first: true, predicate: (RowExpansionDirective), descendants: true, isSignal: true }], viewQueries: [{ propertyName: "cdkTable", first: true, predicate: ["cdkTable"], descendants: true, isSignal: true }], exportAs: ["twTable"], ngImport: i0, template: "<!-- Toolbar slot \u2014 hidden when empty via `empty:hidden` in the slot class. -->\n<div [class]=\"toolbarClasses()\">\n <ng-content select=\"[slot='toolbar']\" />\n</div>\n\n<!-- Scroll wrapper \u2014 also hosts the loading overlay as an absolutely-positioned child. -->\n<div [class]=\"scrollWrapperClasses()\">\n <div [class]=\"scrollContainerClasses()\" [style.max-height]=\"maxHeightStyle()\">\n <table\n #cdkTable=\"cdkTable\"\n cdk-table\n [class]=\"tableClasses()\"\n [attr.aria-label]=\"ariaLabel() ?? null\"\n [attr.aria-labelledby]=\"ariaLabelledby() ?? null\"\n >\n <ng-content select=\"[slot='caption']\" />\n\n <!--\n Emit one <ng-container cdkColumnDef> per visible column. CdkColumnDef's\n @ContentChildren(CdkCellDef / CdkHeaderCellDef / CdkFooterCellDef)\n resolves the <th *cdkHeaderCellDef> / <td *cdkCellDef> / <td *cdkFooterCellDef>\n below \u2014 those renderers delegate to the consumer's projected templates\n (col.cellDef() / col.headerCellDef() / col.footerCellDef()) via\n ngTemplateOutlet.\n -->\n @for (col of visibleColumns(); track col.name()) {\n <ng-container\n [cdkColumnDef]=\"col.name()\"\n [sticky]=\"col.stickyStart()\"\n [stickyEnd]=\"col.stickyEnd()\"\n >\n <th\n cdk-header-cell\n *cdkHeaderCellDef\n [class]=\"col.headerCellClasses()\"\n [style.width]=\"col.resolvedWidth()\"\n [attr.aria-sort]=\"col.ariaSort()\"\n [attr.data-column]=\"col.name()\"\n scope=\"col\"\n >\n @if (col.headerCellDef(); as hdef) {\n <ng-container\n [ngTemplateOutlet]=\"hdef.template\"\n [ngTemplateOutletContext]=\"col.headerContext()\"\n />\n } @else if (col.headerLabel()) {\n {{ col.headerLabel() }}\n }\n </th>\n <td\n cdk-cell\n *cdkCellDef=\"let row; let i = index; let c = count; let f = first; let l = last; let e = even; let o = odd\"\n [class]=\"col.cellClasses()\"\n [style.width]=\"col.resolvedWidth()\"\n [attr.data-label]=\"col.stackDataLabel()\"\n [attr.data-column]=\"col.name()\"\n >\n @if (col.cellDef(); as cdef) {\n <ng-container\n [ngTemplateOutlet]=\"cdef.template\"\n [ngTemplateOutletContext]=\"col.buildCellContext(row, i, c, f, l, e, o)\"\n />\n }\n </td>\n @if (col.footerCellDef()) {\n <td\n cdk-footer-cell\n *cdkFooterCellDef\n [class]=\"col.footerCellClasses()\"\n [style.width]=\"col.resolvedWidth()\"\n [attr.data-column]=\"col.name()\"\n >\n <ng-container\n [ngTemplateOutlet]=\"col.footerCellDef()!.template\"\n [ngTemplateOutletContext]=\"col.footerContext()\"\n />\n </td>\n }\n </ng-container>\n }\n\n <!-- Selection pseudo-column emitted as a leading column when `selection.enabled` is true.\n Hosts the master \"select all\" checkbox in the header and per-row checkboxes in the body.\n The role=\"checkbox\" host on <tw-checkbox> is allowlisted by the row-click suppression\n helper so clicking it does NOT also emit `rowClicked`. -->\n @if (resolvedSelection().enabled) {\n <ng-container cdkColumnDef=\"_selection\">\n <th\n cdk-header-cell\n *cdkHeaderCellDef\n [class]=\"selectionHeaderClasses()\"\n scope=\"col\"\n >\n <tw-checkbox\n size=\"sm\"\n [checked]=\"masterChecked()\"\n [indeterminate]=\"masterIndeterminate()\"\n (change)=\"toggleMasterSelection($event)\"\n [aria-label]=\"resolvedLabels().selectAllLabel\"\n />\n </th>\n <td\n cdk-cell\n *cdkCellDef=\"let row; let i = index\"\n [class]=\"selectionCellClasses()\"\n >\n <tw-checkbox\n size=\"sm\"\n [checked]=\"isSelected(row)\"\n (change)=\"setSelected(row, $event)\"\n [aria-label]=\"selectRowLabel(i)\"\n />\n </td>\n </ng-container>\n }\n\n <!-- Auto-generated row defs driven by visibleColumnNames(). -->\n @if (visibleColumnNames().length > 0) {\n <tr\n cdk-header-row\n *cdkHeaderRowDef=\"visibleColumnNames(); sticky: stickyHeader()\"\n ></tr>\n <tr\n cdk-row\n *cdkRowDef=\"let row; let i = dataIndex; columns: visibleColumnNames()\"\n (click)=\"handleRowClick(row, i, $event)\"\n [attr.aria-selected]=\"rowAriaSelected(row)\"\n ></tr>\n @if (hasFooterRow()) {\n <tr\n cdk-footer-row\n *cdkFooterRowDef=\"visibleColumnNames(); sticky: stickyFooter()\"\n ></tr>\n }\n }\n\n <!-- Expansion pseudo-column + extra row def. -->\n @if (hasExpansion()) {\n <ng-container cdkColumnDef=\"_tw_expansion\">\n <td\n cdk-cell\n *cdkCellDef=\"let row; let i = dataIndex\"\n [attr.colspan]=\"expansionColspan()\"\n [class]=\"expansionCellClasses()\"\n >\n @if (expansionTemplate(); as tpl) {\n <ng-container\n [ngTemplateOutlet]=\"tpl.template\"\n [ngTemplateOutletContext]=\"expansionContext(row, i)\"\n />\n }\n </td>\n </ng-container>\n <tr\n cdk-row\n *cdkRowDef=\"let row; columns: ['_tw_expansion']; when: expansionWhen\"\n [class]=\"expansionRowClasses()\"\n ></tr>\n }\n\n <!-- Consumer-projected content: tw-column metadata components and ng-template-based\n directives (*twNoDataRow, *twRowExpansion). The tw-column hosts are display:none\n so they are harmless inside the <table>. -->\n <ng-content />\n </table>\n\n <!-- Empty-state fallback overlay (hidden when a *twNoDataRow is projected; hidden when loading/error). -->\n @if (shouldRenderEmptyOverlay()) {\n <div [class]=\"emptyStateClasses()\">\n <ng-content select=\"[slot='empty']\">\n <svg\n [class]=\"emptyIconClasses()\"\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"1.5\"\n aria-hidden=\"true\"\n >\n <path\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n d=\"M2.25 13.5h3.86a2.25 2.25 0 0 1 2.012 1.244l.256.512a2.25 2.25 0 0 0 2.013 1.244h3.218a2.25 2.25 0 0 0 2.013-1.244l.256-.512a2.25 2.25 0 0 1 2.013-1.244h3.859m-19.5.338V18a2.25 2.25 0 0 0 2.25 2.25h15A2.25 2.25 0 0 0 21.75 18v-4.162c0-.224-.034-.447-.1-.661l-2.4-8.022A2.25 2.25 0 0 0 17.128 3.5H6.873a2.25 2.25 0 0 0-2.122 1.655l-2.4 8.022a2.25 2.25 0 0 0-.1.661Z\"\n />\n </svg>\n <p [class]=\"emptyMessageClasses()\">{{ resolvedLabels().empty }}</p>\n </ng-content>\n </div>\n }\n\n <!-- Error-state overlay. -->\n @if (shouldRenderErrorOverlay()) {\n <div [class]=\"errorStateClasses()\" role=\"alert\">\n <ng-content select=\"[slot='error']\">\n <svg\n [class]=\"errorIconClasses()\"\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"1.5\"\n aria-hidden=\"true\"\n >\n <path\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n d=\"M12 9v3.75m9-.75a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 3.75h.008v.008H12v-.008Z\"\n />\n </svg>\n <p [class]=\"errorMessageClasses()\">{{ errorMessage() }}</p>\n </ng-content>\n </div>\n }\n\n <!-- Loading overlay (absolute-positioned inside scroll wrapper). -->\n @if (loading()) {\n <div\n [class]=\"loadingOverlayClasses()\"\n role=\"status\"\n aria-live=\"polite\"\n animate.enter=\"fade-in\"\n animate.leave=\"fade-out\"\n >\n <ng-content select=\"[slot='loading']\">\n <svg\n class=\"size-5 shrink-0 animate-spin motion-reduce:animate-none text-fg-muted\"\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n aria-hidden=\"true\"\n >\n <circle\n cx=\"12\"\n cy=\"12\"\n r=\"10\"\n stroke=\"currentColor\"\n stroke-width=\"3\"\n opacity=\"0.2\"\n />\n <circle\n cx=\"12\"\n cy=\"12\"\n r=\"10\"\n stroke=\"currentColor\"\n stroke-width=\"3\"\n stroke-linecap=\"round\"\n stroke-dasharray=\"60\"\n stroke-dashoffset=\"40\"\n />\n </svg>\n <span [class]=\"loadingMessageClasses()\">{{ resolvedLabels().loading }}</span>\n </ng-content>\n </div>\n }\n </div>\n</div>\n\n<!-- Footer slot \u2014 hidden when empty via `empty:hidden` in the slot class. -->\n<div [class]=\"footerSlotClasses()\">\n <ng-content select=\"[slot='footer']\" />\n</div>\n\n<!-- Pagination slot \u2014 hidden when empty via `empty:hidden` in the slot class. -->\n<div [class]=\"paginationSlotClasses()\">\n <ng-content select=\"[slot='pagination']\" />\n</div>\n", dependencies: [{ kind: "ngmodule", type: CdkTableModule }, { kind: "component", type: i1.CdkTable, selector: "cdk-table, table[cdk-table]", inputs: ["trackBy", "dataSource", "multiTemplateDataRows", "fixedLayout", "recycleRows"], outputs: ["contentChanged"], exportAs: ["cdkTable"] }, { kind: "directive", type: i1.CdkRowDef, selector: "[cdkRowDef]", inputs: ["cdkRowDefColumns", "cdkRowDefWhen"] }, { kind: "directive", type: i1.CdkCellDef, selector: "[cdkCellDef]" }, { kind: "directive", type: i1.CdkHeaderCellDef, selector: "[cdkHeaderCellDef]" }, { kind: "directive", type: i1.CdkFooterCellDef, selector: "[cdkFooterCellDef]" }, { kind: "directive", type: i1.CdkColumnDef, selector: "[cdkColumnDef]", inputs: ["cdkColumnDef", "sticky", "stickyEnd"] }, { kind: "directive", type: i1.CdkCell, selector: "cdk-cell, td[cdk-cell]" }, { kind: "component", type: i1.CdkRow, selector: "cdk-row, tr[cdk-row]" }, { kind: "directive", type: i1.CdkHeaderCell, selector: "cdk-header-cell, th[cdk-header-cell]" }, { kind: "directive", type: i1.CdkFooterCell, selector: "cdk-footer-cell, td[cdk-footer-cell]" }, { kind: "component", type: i1.CdkHeaderRow, selector: "cdk-header-row, tr[cdk-header-row]" }, { kind: "directive", type: i1.CdkHeaderRowDef, selector: "[cdkHeaderRowDef]", inputs: ["cdkHeaderRowDef", "cdkHeaderRowDefSticky"] }, { kind: "component", type: i1.CdkFooterRow, selector: "cdk-footer-row, tr[cdk-footer-row]" }, { kind: "directive", type: i1.CdkFooterRowDef, selector: "[cdkFooterRowDef]", inputs: ["cdkFooterRowDef", "cdkFooterRowDefSticky"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: CheckboxComponent, selector: "tw-checkbox", inputs: ["color", "size", "variant", "disabled", "required", "label", "description", "labelPosition", "name", "id", "aria-label", "aria-labelledby", "aria-describedby", "errorStateMatcher", "checked", "indeterminate"], outputs: ["checkedChange", "indeterminateChange", "change"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
981
+ }
982
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: TableComponent, decorators: [{
983
+ type: Component,
984
+ args: [{ selector: 'tw-table', exportAs: 'twTable', changeDetection: ChangeDetectionStrategy.OnPush, imports: [CdkTableModule, NgTemplateOutlet, CheckboxComponent], host: {
985
+ '[class]': 'hostClasses()',
986
+ }, template: "<!-- Toolbar slot \u2014 hidden when empty via `empty:hidden` in the slot class. -->\n<div [class]=\"toolbarClasses()\">\n <ng-content select=\"[slot='toolbar']\" />\n</div>\n\n<!-- Scroll wrapper \u2014 also hosts the loading overlay as an absolutely-positioned child. -->\n<div [class]=\"scrollWrapperClasses()\">\n <div [class]=\"scrollContainerClasses()\" [style.max-height]=\"maxHeightStyle()\">\n <table\n #cdkTable=\"cdkTable\"\n cdk-table\n [class]=\"tableClasses()\"\n [attr.aria-label]=\"ariaLabel() ?? null\"\n [attr.aria-labelledby]=\"ariaLabelledby() ?? null\"\n >\n <ng-content select=\"[slot='caption']\" />\n\n <!--\n Emit one <ng-container cdkColumnDef> per visible column. CdkColumnDef's\n @ContentChildren(CdkCellDef / CdkHeaderCellDef / CdkFooterCellDef)\n resolves the <th *cdkHeaderCellDef> / <td *cdkCellDef> / <td *cdkFooterCellDef>\n below \u2014 those renderers delegate to the consumer's projected templates\n (col.cellDef() / col.headerCellDef() / col.footerCellDef()) via\n ngTemplateOutlet.\n -->\n @for (col of visibleColumns(); track col.name()) {\n <ng-container\n [cdkColumnDef]=\"col.name()\"\n [sticky]=\"col.stickyStart()\"\n [stickyEnd]=\"col.stickyEnd()\"\n >\n <th\n cdk-header-cell\n *cdkHeaderCellDef\n [class]=\"col.headerCellClasses()\"\n [style.width]=\"col.resolvedWidth()\"\n [attr.aria-sort]=\"col.ariaSort()\"\n [attr.data-column]=\"col.name()\"\n scope=\"col\"\n >\n @if (col.headerCellDef(); as hdef) {\n <ng-container\n [ngTemplateOutlet]=\"hdef.template\"\n [ngTemplateOutletContext]=\"col.headerContext()\"\n />\n } @else if (col.headerLabel()) {\n {{ col.headerLabel() }}\n }\n </th>\n <td\n cdk-cell\n *cdkCellDef=\"let row; let i = index; let c = count; let f = first; let l = last; let e = even; let o = odd\"\n [class]=\"col.cellClasses()\"\n [style.width]=\"col.resolvedWidth()\"\n [attr.data-label]=\"col.stackDataLabel()\"\n [attr.data-column]=\"col.name()\"\n >\n @if (col.cellDef(); as cdef) {\n <ng-container\n [ngTemplateOutlet]=\"cdef.template\"\n [ngTemplateOutletContext]=\"col.buildCellContext(row, i, c, f, l, e, o)\"\n />\n }\n </td>\n @if (col.footerCellDef()) {\n <td\n cdk-footer-cell\n *cdkFooterCellDef\n [class]=\"col.footerCellClasses()\"\n [style.width]=\"col.resolvedWidth()\"\n [attr.data-column]=\"col.name()\"\n >\n <ng-container\n [ngTemplateOutlet]=\"col.footerCellDef()!.template\"\n [ngTemplateOutletContext]=\"col.footerContext()\"\n />\n </td>\n }\n </ng-container>\n }\n\n <!-- Selection pseudo-column emitted as a leading column when `selection.enabled` is true.\n Hosts the master \"select all\" checkbox in the header and per-row checkboxes in the body.\n The role=\"checkbox\" host on <tw-checkbox> is allowlisted by the row-click suppression\n helper so clicking it does NOT also emit `rowClicked`. -->\n @if (resolvedSelection().enabled) {\n <ng-container cdkColumnDef=\"_selection\">\n <th\n cdk-header-cell\n *cdkHeaderCellDef\n [class]=\"selectionHeaderClasses()\"\n scope=\"col\"\n >\n <tw-checkbox\n size=\"sm\"\n [checked]=\"masterChecked()\"\n [indeterminate]=\"masterIndeterminate()\"\n (change)=\"toggleMasterSelection($event)\"\n [aria-label]=\"resolvedLabels().selectAllLabel\"\n />\n </th>\n <td\n cdk-cell\n *cdkCellDef=\"let row; let i = index\"\n [class]=\"selectionCellClasses()\"\n >\n <tw-checkbox\n size=\"sm\"\n [checked]=\"isSelected(row)\"\n (change)=\"setSelected(row, $event)\"\n [aria-label]=\"selectRowLabel(i)\"\n />\n </td>\n </ng-container>\n }\n\n <!-- Auto-generated row defs driven by visibleColumnNames(). -->\n @if (visibleColumnNames().length > 0) {\n <tr\n cdk-header-row\n *cdkHeaderRowDef=\"visibleColumnNames(); sticky: stickyHeader()\"\n ></tr>\n <tr\n cdk-row\n *cdkRowDef=\"let row; let i = dataIndex; columns: visibleColumnNames()\"\n (click)=\"handleRowClick(row, i, $event)\"\n [attr.aria-selected]=\"rowAriaSelected(row)\"\n ></tr>\n @if (hasFooterRow()) {\n <tr\n cdk-footer-row\n *cdkFooterRowDef=\"visibleColumnNames(); sticky: stickyFooter()\"\n ></tr>\n }\n }\n\n <!-- Expansion pseudo-column + extra row def. -->\n @if (hasExpansion()) {\n <ng-container cdkColumnDef=\"_tw_expansion\">\n <td\n cdk-cell\n *cdkCellDef=\"let row; let i = dataIndex\"\n [attr.colspan]=\"expansionColspan()\"\n [class]=\"expansionCellClasses()\"\n >\n @if (expansionTemplate(); as tpl) {\n <ng-container\n [ngTemplateOutlet]=\"tpl.template\"\n [ngTemplateOutletContext]=\"expansionContext(row, i)\"\n />\n }\n </td>\n </ng-container>\n <tr\n cdk-row\n *cdkRowDef=\"let row; columns: ['_tw_expansion']; when: expansionWhen\"\n [class]=\"expansionRowClasses()\"\n ></tr>\n }\n\n <!-- Consumer-projected content: tw-column metadata components and ng-template-based\n directives (*twNoDataRow, *twRowExpansion). The tw-column hosts are display:none\n so they are harmless inside the <table>. -->\n <ng-content />\n </table>\n\n <!-- Empty-state fallback overlay (hidden when a *twNoDataRow is projected; hidden when loading/error). -->\n @if (shouldRenderEmptyOverlay()) {\n <div [class]=\"emptyStateClasses()\">\n <ng-content select=\"[slot='empty']\">\n <svg\n [class]=\"emptyIconClasses()\"\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"1.5\"\n aria-hidden=\"true\"\n >\n <path\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n d=\"M2.25 13.5h3.86a2.25 2.25 0 0 1 2.012 1.244l.256.512a2.25 2.25 0 0 0 2.013 1.244h3.218a2.25 2.25 0 0 0 2.013-1.244l.256-.512a2.25 2.25 0 0 1 2.013-1.244h3.859m-19.5.338V18a2.25 2.25 0 0 0 2.25 2.25h15A2.25 2.25 0 0 0 21.75 18v-4.162c0-.224-.034-.447-.1-.661l-2.4-8.022A2.25 2.25 0 0 0 17.128 3.5H6.873a2.25 2.25 0 0 0-2.122 1.655l-2.4 8.022a2.25 2.25 0 0 0-.1.661Z\"\n />\n </svg>\n <p [class]=\"emptyMessageClasses()\">{{ resolvedLabels().empty }}</p>\n </ng-content>\n </div>\n }\n\n <!-- Error-state overlay. -->\n @if (shouldRenderErrorOverlay()) {\n <div [class]=\"errorStateClasses()\" role=\"alert\">\n <ng-content select=\"[slot='error']\">\n <svg\n [class]=\"errorIconClasses()\"\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"1.5\"\n aria-hidden=\"true\"\n >\n <path\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n d=\"M12 9v3.75m9-.75a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 3.75h.008v.008H12v-.008Z\"\n />\n </svg>\n <p [class]=\"errorMessageClasses()\">{{ errorMessage() }}</p>\n </ng-content>\n </div>\n }\n\n <!-- Loading overlay (absolute-positioned inside scroll wrapper). -->\n @if (loading()) {\n <div\n [class]=\"loadingOverlayClasses()\"\n role=\"status\"\n aria-live=\"polite\"\n animate.enter=\"fade-in\"\n animate.leave=\"fade-out\"\n >\n <ng-content select=\"[slot='loading']\">\n <svg\n class=\"size-5 shrink-0 animate-spin motion-reduce:animate-none text-fg-muted\"\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n aria-hidden=\"true\"\n >\n <circle\n cx=\"12\"\n cy=\"12\"\n r=\"10\"\n stroke=\"currentColor\"\n stroke-width=\"3\"\n opacity=\"0.2\"\n />\n <circle\n cx=\"12\"\n cy=\"12\"\n r=\"10\"\n stroke=\"currentColor\"\n stroke-width=\"3\"\n stroke-linecap=\"round\"\n stroke-dasharray=\"60\"\n stroke-dashoffset=\"40\"\n />\n </svg>\n <span [class]=\"loadingMessageClasses()\">{{ resolvedLabels().loading }}</span>\n </ng-content>\n </div>\n }\n </div>\n</div>\n\n<!-- Footer slot \u2014 hidden when empty via `empty:hidden` in the slot class. -->\n<div [class]=\"footerSlotClasses()\">\n <ng-content select=\"[slot='footer']\" />\n</div>\n\n<!-- Pagination slot \u2014 hidden when empty via `empty:hidden` in the slot class. -->\n<div [class]=\"paginationSlotClasses()\">\n <ng-content select=\"[slot='pagination']\" />\n</div>\n" }]
987
+ }], ctorParameters: () => [], propDecorators: { data: [{ type: i0.Input, args: [{ isSignal: true, alias: "data", required: false }] }], trackBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "trackBy", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], error: [{ type: i0.Input, args: [{ isSignal: true, alias: "error", required: false }] }], appearance: [{ type: i0.Input, args: [{ isSignal: true, alias: "appearance", required: false }] }], sticky: [{ type: i0.Input, args: [{ isSignal: true, alias: "sticky", required: false }] }], responsive: [{ type: i0.Input, args: [{ isSignal: true, alias: "responsive", required: false }] }], selection: [{ type: i0.Input, args: [{ isSignal: true, alias: "selection", required: false }] }], multiTemplateRows: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiTemplateRows", required: false }] }], labels: [{ type: i0.Input, args: [{ isSignal: true, alias: "labels", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "aria-label", required: false }] }], ariaLabelledby: [{ type: i0.Input, args: [{ isSignal: true, alias: "aria-labelledby", required: false }] }], expandedRows: [{ type: i0.Input, args: [{ isSignal: true, alias: "expandedRows", required: false }] }, { type: i0.Output, args: ["expandedRowsChange"] }], selected: [{ type: i0.Input, args: [{ isSignal: true, alias: "selected", required: false }] }, { type: i0.Output, args: ["selectedChange"] }], rowClicked: [{ type: i0.Output, args: ["rowClicked"] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }], expansionChange: [{ type: i0.Output, args: ["expansionChange"] }], columns: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => ColumnComponent), { ...{ descendants: true }, isSignal: true }] }], noDataRow: [{ type: i0.ContentChild, args: [i0.forwardRef(() => NoDataRowDirective), { ...{ descendants: true }, isSignal: true }] }], expansionTemplate: [{ type: i0.ContentChild, args: [i0.forwardRef(() => RowExpansionDirective), { ...{ descendants: true }, isSignal: true }] }], cdkTable: [{ type: i0.ViewChild, args: ['cdkTable', { isSignal: true }] }] } });
988
+
989
+ /**
990
+ * Generated bundle index. Do not edit.
991
+ */
992
+
993
+ export { CellDefDirective, ColumnComponent, DEFAULT_TABLE_LABELS, FooterCellDefDirective, HeaderCellDefDirective, NoDataRowDirective, RowExpansionDirective, TableComponent };
994
+ //# sourceMappingURL=cdevhub-ngx-tw-table.mjs.map