@cdevhub/ngx-tw 0.4.0 → 0.6.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 (209) hide show
  1. package/README.md +78 -15
  2. package/fesm2022/cdevhub-ngx-tw-accordion.mjs +16 -5
  3. package/fesm2022/cdevhub-ngx-tw-accordion.mjs.map +1 -1
  4. package/fesm2022/cdevhub-ngx-tw-avatar.mjs +26 -15
  5. package/fesm2022/cdevhub-ngx-tw-avatar.mjs.map +1 -1
  6. package/fesm2022/cdevhub-ngx-tw-badge.mjs +17 -5
  7. package/fesm2022/cdevhub-ngx-tw-badge.mjs.map +1 -1
  8. package/fesm2022/cdevhub-ngx-tw-breadcrumbs.mjs +27 -6
  9. package/fesm2022/cdevhub-ngx-tw-breadcrumbs.mjs.map +1 -1
  10. package/fesm2022/cdevhub-ngx-tw-button.mjs +23 -11
  11. package/fesm2022/cdevhub-ngx-tw-button.mjs.map +1 -1
  12. package/fesm2022/cdevhub-ngx-tw-calendar-luxon.mjs +5 -5
  13. package/fesm2022/cdevhub-ngx-tw-calendar-luxon.mjs.map +1 -1
  14. package/fesm2022/cdevhub-ngx-tw-calendar-testing.mjs +3 -3
  15. package/fesm2022/cdevhub-ngx-tw-calendar-testing.mjs.map +1 -1
  16. package/fesm2022/cdevhub-ngx-tw-calendar.mjs +427 -122
  17. package/fesm2022/cdevhub-ngx-tw-calendar.mjs.map +1 -1
  18. package/fesm2022/cdevhub-ngx-tw-card.mjs +21 -11
  19. package/fesm2022/cdevhub-ngx-tw-card.mjs.map +1 -1
  20. package/fesm2022/cdevhub-ngx-tw-carousel.mjs +215 -96
  21. package/fesm2022/cdevhub-ngx-tw-carousel.mjs.map +1 -1
  22. package/fesm2022/cdevhub-ngx-tw-checkbox.mjs +108 -64
  23. package/fesm2022/cdevhub-ngx-tw-checkbox.mjs.map +1 -1
  24. package/fesm2022/cdevhub-ngx-tw-code-block.mjs +18 -6
  25. package/fesm2022/cdevhub-ngx-tw-code-block.mjs.map +1 -1
  26. package/fesm2022/cdevhub-ngx-tw-collapsible.mjs +55 -30
  27. package/fesm2022/cdevhub-ngx-tw-collapsible.mjs.map +1 -1
  28. package/fesm2022/cdevhub-ngx-tw-combobox-testing.mjs +218 -0
  29. package/fesm2022/cdevhub-ngx-tw-combobox-testing.mjs.map +1 -0
  30. package/fesm2022/cdevhub-ngx-tw-combobox.mjs +236 -211
  31. package/fesm2022/cdevhub-ngx-tw-combobox.mjs.map +1 -1
  32. package/fesm2022/cdevhub-ngx-tw-command-palette-testing.mjs +200 -0
  33. package/fesm2022/cdevhub-ngx-tw-command-palette-testing.mjs.map +1 -0
  34. package/fesm2022/cdevhub-ngx-tw-command-palette.mjs +34 -7
  35. package/fesm2022/cdevhub-ngx-tw-command-palette.mjs.map +1 -1
  36. package/fesm2022/cdevhub-ngx-tw-core.mjs +135 -15
  37. package/fesm2022/cdevhub-ngx-tw-core.mjs.map +1 -1
  38. package/fesm2022/cdevhub-ngx-tw-date-picker-testing.mjs +203 -0
  39. package/fesm2022/cdevhub-ngx-tw-date-picker-testing.mjs.map +1 -0
  40. package/fesm2022/cdevhub-ngx-tw-date-picker.mjs +160 -102
  41. package/fesm2022/cdevhub-ngx-tw-date-picker.mjs.map +1 -1
  42. package/fesm2022/cdevhub-ngx-tw-date-range-picker-testing.mjs +209 -0
  43. package/fesm2022/cdevhub-ngx-tw-date-range-picker-testing.mjs.map +1 -0
  44. package/fesm2022/cdevhub-ngx-tw-date-range-picker.mjs +182 -107
  45. package/fesm2022/cdevhub-ngx-tw-date-range-picker.mjs.map +1 -1
  46. package/fesm2022/cdevhub-ngx-tw-dialog-dialog-renderer-DoIhoV3d.mjs +198 -0
  47. package/fesm2022/cdevhub-ngx-tw-dialog-dialog-renderer-DoIhoV3d.mjs.map +1 -0
  48. package/fesm2022/cdevhub-ngx-tw-dialog-testing.mjs +122 -0
  49. package/fesm2022/cdevhub-ngx-tw-dialog-testing.mjs.map +1 -0
  50. package/fesm2022/cdevhub-ngx-tw-dialog.mjs +199 -220
  51. package/fesm2022/cdevhub-ngx-tw-dialog.mjs.map +1 -1
  52. package/fesm2022/cdevhub-ngx-tw-empty-state.mjs +1 -1
  53. package/fesm2022/cdevhub-ngx-tw-empty-state.mjs.map +1 -1
  54. package/fesm2022/cdevhub-ngx-tw-file-upload-testing.mjs +149 -0
  55. package/fesm2022/cdevhub-ngx-tw-file-upload-testing.mjs.map +1 -0
  56. package/fesm2022/cdevhub-ngx-tw-file-upload.mjs +57 -67
  57. package/fesm2022/cdevhub-ngx-tw-file-upload.mjs.map +1 -1
  58. package/fesm2022/cdevhub-ngx-tw-flip-card.mjs +42 -24
  59. package/fesm2022/cdevhub-ngx-tw-flip-card.mjs.map +1 -1
  60. package/fesm2022/cdevhub-ngx-tw-form-field.mjs +114 -21
  61. package/fesm2022/cdevhub-ngx-tw-form-field.mjs.map +1 -1
  62. package/fesm2022/cdevhub-ngx-tw-icon.mjs +16 -6
  63. package/fesm2022/cdevhub-ngx-tw-icon.mjs.map +1 -1
  64. package/fesm2022/cdevhub-ngx-tw-input.mjs +52 -61
  65. package/fesm2022/cdevhub-ngx-tw-input.mjs.map +1 -1
  66. package/fesm2022/cdevhub-ngx-tw-item.mjs +18 -3
  67. package/fesm2022/cdevhub-ngx-tw-item.mjs.map +1 -1
  68. package/fesm2022/cdevhub-ngx-tw-menu-testing.mjs +168 -0
  69. package/fesm2022/cdevhub-ngx-tw-menu-testing.mjs.map +1 -0
  70. package/fesm2022/cdevhub-ngx-tw-menu.mjs +58 -21
  71. package/fesm2022/cdevhub-ngx-tw-menu.mjs.map +1 -1
  72. package/fesm2022/cdevhub-ngx-tw-number-input.mjs +34 -1
  73. package/fesm2022/cdevhub-ngx-tw-number-input.mjs.map +1 -1
  74. package/fesm2022/cdevhub-ngx-tw-paginator.mjs +95 -45
  75. package/fesm2022/cdevhub-ngx-tw-paginator.mjs.map +1 -1
  76. package/fesm2022/cdevhub-ngx-tw-popover.mjs +81 -32
  77. package/fesm2022/cdevhub-ngx-tw-popover.mjs.map +1 -1
  78. package/fesm2022/cdevhub-ngx-tw-progress-bar.mjs +1 -1
  79. package/fesm2022/cdevhub-ngx-tw-progress-bar.mjs.map +1 -1
  80. package/fesm2022/cdevhub-ngx-tw-radio.mjs +131 -98
  81. package/fesm2022/cdevhub-ngx-tw-radio.mjs.map +1 -1
  82. package/fesm2022/cdevhub-ngx-tw-segmented-control.mjs +207 -42
  83. package/fesm2022/cdevhub-ngx-tw-segmented-control.mjs.map +1 -1
  84. package/fesm2022/cdevhub-ngx-tw-select-testing.mjs +165 -0
  85. package/fesm2022/cdevhub-ngx-tw-select-testing.mjs.map +1 -0
  86. package/fesm2022/cdevhub-ngx-tw-select.mjs +386 -210
  87. package/fesm2022/cdevhub-ngx-tw-select.mjs.map +1 -1
  88. package/fesm2022/cdevhub-ngx-tw-sheet-sheet-renderer-CqVJwgmp.mjs +256 -0
  89. package/fesm2022/cdevhub-ngx-tw-sheet-sheet-renderer-CqVJwgmp.mjs.map +1 -0
  90. package/fesm2022/cdevhub-ngx-tw-sheet-testing.mjs +129 -0
  91. package/fesm2022/cdevhub-ngx-tw-sheet-testing.mjs.map +1 -0
  92. package/fesm2022/cdevhub-ngx-tw-sheet.mjs +197 -278
  93. package/fesm2022/cdevhub-ngx-tw-sheet.mjs.map +1 -1
  94. package/fesm2022/cdevhub-ngx-tw-slider.mjs +89 -54
  95. package/fesm2022/cdevhub-ngx-tw-slider.mjs.map +1 -1
  96. package/fesm2022/cdevhub-ngx-tw-sort.mjs +122 -17
  97. package/fesm2022/cdevhub-ngx-tw-sort.mjs.map +1 -1
  98. package/fesm2022/cdevhub-ngx-tw-split.mjs +72 -19
  99. package/fesm2022/cdevhub-ngx-tw-split.mjs.map +1 -1
  100. package/fesm2022/cdevhub-ngx-tw-stat.mjs +63 -25
  101. package/fesm2022/cdevhub-ngx-tw-stat.mjs.map +1 -1
  102. package/fesm2022/cdevhub-ngx-tw-stepper.mjs +35 -8
  103. package/fesm2022/cdevhub-ngx-tw-stepper.mjs.map +1 -1
  104. package/fesm2022/cdevhub-ngx-tw-switch.mjs +54 -48
  105. package/fesm2022/cdevhub-ngx-tw-switch.mjs.map +1 -1
  106. package/fesm2022/cdevhub-ngx-tw-tab-nav.mjs +12 -15
  107. package/fesm2022/cdevhub-ngx-tw-tab-nav.mjs.map +1 -1
  108. package/fesm2022/cdevhub-ngx-tw-table.mjs +74 -13
  109. package/fesm2022/cdevhub-ngx-tw-table.mjs.map +1 -1
  110. package/fesm2022/cdevhub-ngx-tw-tabs.mjs +84 -30
  111. package/fesm2022/cdevhub-ngx-tw-tabs.mjs.map +1 -1
  112. package/fesm2022/cdevhub-ngx-tw-tags-input-testing.mjs +172 -0
  113. package/fesm2022/cdevhub-ngx-tw-tags-input-testing.mjs.map +1 -0
  114. package/fesm2022/cdevhub-ngx-tw-tags-input.mjs +107 -71
  115. package/fesm2022/cdevhub-ngx-tw-tags-input.mjs.map +1 -1
  116. package/fesm2022/cdevhub-ngx-tw-textarea.mjs +41 -9
  117. package/fesm2022/cdevhub-ngx-tw-textarea.mjs.map +1 -1
  118. package/fesm2022/cdevhub-ngx-tw-theme.mjs +257 -45
  119. package/fesm2022/cdevhub-ngx-tw-theme.mjs.map +1 -1
  120. package/fesm2022/cdevhub-ngx-tw-time-picker-testing.mjs +201 -0
  121. package/fesm2022/cdevhub-ngx-tw-time-picker-testing.mjs.map +1 -0
  122. package/fesm2022/cdevhub-ngx-tw-time-picker.mjs +184 -77
  123. package/fesm2022/cdevhub-ngx-tw-time-picker.mjs.map +1 -1
  124. package/fesm2022/cdevhub-ngx-tw-timeline.mjs +110 -27
  125. package/fesm2022/cdevhub-ngx-tw-timeline.mjs.map +1 -1
  126. package/fesm2022/cdevhub-ngx-tw-toast-toast-renderer-BlDxemsU.mjs +616 -0
  127. package/fesm2022/cdevhub-ngx-tw-toast-toast-renderer-BlDxemsU.mjs.map +1 -0
  128. package/fesm2022/cdevhub-ngx-tw-toast.mjs +556 -945
  129. package/fesm2022/cdevhub-ngx-tw-toast.mjs.map +1 -1
  130. package/fesm2022/cdevhub-ngx-tw-tooltip.mjs +102 -15
  131. package/fesm2022/cdevhub-ngx-tw-tooltip.mjs.map +1 -1
  132. package/fesm2022/cdevhub-ngx-tw-transfer-testing.mjs +219 -0
  133. package/fesm2022/cdevhub-ngx-tw-transfer-testing.mjs.map +1 -0
  134. package/fesm2022/cdevhub-ngx-tw-transfer.mjs +88 -69
  135. package/fesm2022/cdevhub-ngx-tw-transfer.mjs.map +1 -1
  136. package/fesm2022/cdevhub-ngx-tw-tree.mjs +68 -19
  137. package/fesm2022/cdevhub-ngx-tw-tree.mjs.map +1 -1
  138. package/index.json +1 -1
  139. package/package.json +63 -2
  140. package/theme/_base.css +13 -0
  141. package/theme/_dark.css +110 -36
  142. package/theme/_high-contrast-dark.css +367 -0
  143. package/theme/_high-contrast.css +13 -2
  144. package/theme/_light.css +276 -0
  145. package/theme/_semantic.css +47 -12
  146. package/theme/_typography.css +17 -0
  147. package/theme/index.css +86 -7
  148. package/types/cdevhub-ngx-tw-accordion.d.ts +12 -4
  149. package/types/cdevhub-ngx-tw-avatar.d.ts +10 -3
  150. package/types/cdevhub-ngx-tw-button.d.ts +1 -0
  151. package/types/cdevhub-ngx-tw-calendar-luxon.d.ts +2 -2
  152. package/types/cdevhub-ngx-tw-calendar-testing.d.ts +1 -1
  153. package/types/cdevhub-ngx-tw-calendar.d.ts +179 -73
  154. package/types/cdevhub-ngx-tw-card.d.ts +11 -3
  155. package/types/cdevhub-ngx-tw-carousel.d.ts +89 -20
  156. package/types/cdevhub-ngx-tw-checkbox.d.ts +40 -12
  157. package/types/cdevhub-ngx-tw-code-block.d.ts +11 -2
  158. package/types/cdevhub-ngx-tw-collapsible.d.ts +32 -5
  159. package/types/cdevhub-ngx-tw-combobox-testing.d.ts +157 -0
  160. package/types/cdevhub-ngx-tw-combobox.d.ts +94 -53
  161. package/types/cdevhub-ngx-tw-command-palette-testing.d.ts +136 -0
  162. package/types/cdevhub-ngx-tw-command-palette.d.ts +33 -5
  163. package/types/cdevhub-ngx-tw-core.d.ts +152 -21
  164. package/types/cdevhub-ngx-tw-date-picker-testing.d.ts +141 -0
  165. package/types/cdevhub-ngx-tw-date-picker.d.ts +37 -29
  166. package/types/cdevhub-ngx-tw-date-range-picker-testing.d.ts +148 -0
  167. package/types/cdevhub-ngx-tw-date-range-picker.d.ts +36 -29
  168. package/types/cdevhub-ngx-tw-dialog-testing.d.ts +98 -0
  169. package/types/cdevhub-ngx-tw-dialog.d.ts +89 -19
  170. package/types/cdevhub-ngx-tw-file-upload-testing.d.ts +120 -0
  171. package/types/cdevhub-ngx-tw-file-upload.d.ts +8 -10
  172. package/types/cdevhub-ngx-tw-flip-card.d.ts +18 -11
  173. package/types/cdevhub-ngx-tw-form-field.d.ts +19 -2
  174. package/types/cdevhub-ngx-tw-icon.d.ts +3 -3
  175. package/types/cdevhub-ngx-tw-input.d.ts +5 -8
  176. package/types/cdevhub-ngx-tw-item.d.ts +2 -0
  177. package/types/cdevhub-ngx-tw-menu-testing.d.ts +131 -0
  178. package/types/cdevhub-ngx-tw-menu.d.ts +2 -2
  179. package/types/cdevhub-ngx-tw-number-input.d.ts +20 -0
  180. package/types/cdevhub-ngx-tw-paginator.d.ts +58 -24
  181. package/types/cdevhub-ngx-tw-popover.d.ts +41 -10
  182. package/types/cdevhub-ngx-tw-progress-bar.d.ts +1 -1
  183. package/types/cdevhub-ngx-tw-radio.d.ts +68 -25
  184. package/types/cdevhub-ngx-tw-segmented-control.d.ts +79 -16
  185. package/types/cdevhub-ngx-tw-select-testing.d.ts +114 -0
  186. package/types/cdevhub-ngx-tw-select.d.ts +104 -33
  187. package/types/cdevhub-ngx-tw-sheet-testing.d.ts +104 -0
  188. package/types/cdevhub-ngx-tw-sheet.d.ts +96 -21
  189. package/types/cdevhub-ngx-tw-slider.d.ts +79 -25
  190. package/types/cdevhub-ngx-tw-sort.d.ts +45 -4
  191. package/types/cdevhub-ngx-tw-split.d.ts +32 -2
  192. package/types/cdevhub-ngx-tw-stat.d.ts +14 -4
  193. package/types/cdevhub-ngx-tw-stepper.d.ts +16 -2
  194. package/types/cdevhub-ngx-tw-switch.d.ts +35 -13
  195. package/types/cdevhub-ngx-tw-tab-nav.d.ts +7 -6
  196. package/types/cdevhub-ngx-tw-table.d.ts +53 -10
  197. package/types/cdevhub-ngx-tw-tabs.d.ts +41 -10
  198. package/types/cdevhub-ngx-tw-tags-input-testing.d.ts +135 -0
  199. package/types/cdevhub-ngx-tw-tags-input.d.ts +26 -11
  200. package/types/cdevhub-ngx-tw-textarea.d.ts +3 -2
  201. package/types/cdevhub-ngx-tw-theme.d.ts +245 -39
  202. package/types/cdevhub-ngx-tw-time-picker-testing.d.ts +129 -0
  203. package/types/cdevhub-ngx-tw-time-picker.d.ts +76 -19
  204. package/types/cdevhub-ngx-tw-timeline.d.ts +36 -12
  205. package/types/cdevhub-ngx-tw-toast.d.ts +61 -12
  206. package/types/cdevhub-ngx-tw-tooltip.d.ts +28 -6
  207. package/types/cdevhub-ngx-tw-transfer-testing.d.ts +152 -0
  208. package/types/cdevhub-ngx-tw-transfer.d.ts +21 -8
  209. package/types/cdevhub-ngx-tw-tree.d.ts +6 -1
@@ -1,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { model, input, output, isDevMode, Directive, inject, viewChild, effect, computed, ChangeDetectionStrategy, Component } from '@angular/core';
2
+ import { model, input, output, isDevMode, Directive, inject, ElementRef, viewChild, effect, computed, ChangeDetectionStrategy, Component } from '@angular/core';
3
3
  import { TW_SORT_HANDLE } from '@cdevhub/ngx-tw/core';
4
4
  import { NgTemplateOutlet } from '@angular/common';
5
5
  import { FocusMonitor, AriaDescriber } from '@angular/cdk/a11y';
@@ -35,13 +35,13 @@ class SortDirective {
35
35
  active = model(null, { ...(ngDevMode ? { debugName: "active" } : /* istanbul ignore next */ {}), alias: 'twSortActive' });
36
36
  /** Current sort direction. `null` represents the cleared state. Two-way bindable via `[(twSortDirection)]`. Defaults to `null`. */
37
37
  direction = model(null, { ...(ngDevMode ? { debugName: "direction" } : /* istanbul ignore next */ {}), alias: 'twSortDirection' });
38
- /** Starting direction used when a header becomes active for the first time. Per-header `start` overrides this. Defaults to `'asc'`. */
38
+ /** Starting direction used when a header becomes active for the first time. Per-header `start` overrides this. Defaults to `'asc'`. Alias: `twSortStart`. */
39
39
  start = input('asc', { ...(ngDevMode ? { debugName: "start" } : /* istanbul ignore next */ {}), alias: 'twSortStart' });
40
- /** When true, the direction cycle skips the cleared (`null`) state — headers toggle between `'asc'` and `'desc'` only. Per-header `disableClear` overrides this. Defaults to `false`. */
40
+ /** When true, the direction cycle skips the cleared (`null`) state — headers toggle between `'asc'` and `'desc'` only. Per-header `disableClear` overrides this. Defaults to `false`. Alias: `twSortDisableClear`. */
41
41
  disableClear = input(false, { ...(ngDevMode ? { debugName: "disableClear" } : /* istanbul ignore next */ {}), alias: 'twSortDisableClear' });
42
- /** When true, all child sort headers are disabled. Defaults to `false`. */
42
+ /** When true, all child sort headers are disabled. Defaults to `false`. Alias: `twSortDisabled`. */
43
43
  disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), alias: 'twSortDisabled' });
44
- /** Fires whenever the user changes `active` or `direction` by interacting with a header. Programmatic writes to `[(twSortActive)]` / `[(twSortDirection)]` do NOT emit. */
44
+ /** Fires whenever the user changes `active` or `direction` by interacting with a header. Programmatic writes to `[(twSortActive)]` / `[(twSortDirection)]` do NOT emit. Alias: `twSortChange`. */
45
45
  sortChange = output({ alias: 'twSortChange' });
46
46
  sortableIds = new Set();
47
47
  /** Registers a header id so duplicates can be detected. Called by `SortHeaderComponent` on init. Throws in dev mode if another header with the same id is already registered. */
@@ -125,6 +125,29 @@ function snapshot(self) {
125
125
  disabled: self.isDisabled(),
126
126
  };
127
127
  }
128
+ /**
129
+ * Whether the host element exposes a `columnheader` / `rowheader` role, which are the only two
130
+ * roles `aria-sort` is valid on. A static `role` attribute wins over the tag name; a `[attr.role]`
131
+ * *binding* is not visible yet at construction time and is deliberately not consulted.
132
+ */
133
+ function isHeaderCell(el) {
134
+ const role = el.getAttribute('role');
135
+ if (role !== null)
136
+ return role === 'columnheader' || role === 'rowheader';
137
+ return el.tagName === 'TH';
138
+ }
139
+ /**
140
+ * Whether the host element is already an interactive control. When it is, the inner container must
141
+ * NOT also become one — two nested widgets is an axe `nested-interactive` failure and leaves
142
+ * assistive tech with an ambiguous activation target.
143
+ */
144
+ function isInteractiveHost(el) {
145
+ const role = el.getAttribute('role');
146
+ if (role === 'button' || role === 'link')
147
+ return true;
148
+ const tag = el.tagName;
149
+ return tag === 'BUTTON' || (tag === 'A' && el.hasAttribute('href'));
150
+ }
128
151
  // ── Static per-color maps (Tailwind v4 requires literal class strings for its JIT scanner). ──
129
152
  const ARROW_ACTIVE_COLOR = {
130
153
  primary: 'text-primary-600',
@@ -140,33 +163,44 @@ const ARROW_ACTIVE_COLOR = {
140
163
  const sortHeaderVariants = tv({
141
164
  slots: {
142
165
  host: 'group select-none',
143
- container: 'inline-flex items-center gap-1.5 cursor-pointer rounded-md font-medium text-fg transition-colors duration-normal motion-reduce:transition-none hover:bg-surface-muted focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500',
166
+ // `align-top` is load-bearing, not cosmetic. The container is an
167
+ // inline-level box inside the host block, so without it the host's line
168
+ // box is `strut ∪ baseline-aligned container` — at `xs` the strut's
169
+ // descent hangs 2px below the 24px control, and the row measures 26px.
170
+ // `vertical-align: top` flushes the box to the line-box top, making the
171
+ // host exactly the pinned height at every size. Inert at sm–xl, where the
172
+ // control already exceeds the strut.
173
+ container: 'inline-flex align-top items-center gap-1.5 cursor-pointer rounded-md font-medium text-fg transition-colors duration-normal motion-reduce:transition-none hover:bg-surface-muted focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500',
144
174
  label: 'min-w-0',
145
175
  arrow: 'inline-flex shrink-0 items-center justify-center transition-opacity duration-normal motion-reduce:transition-none',
146
176
  arrowIcon: 'shrink-0 transition-transform duration-normal motion-reduce:transition-none',
147
177
  },
148
178
  variants: {
149
179
  size: {
180
+ // Heights are pinned to the control scale (docs/vertical-rhythm.md):
181
+ // 24 / 32 / 36 / 44 / 48. Vertical padding is deleted deliberately —
182
+ // `items-center` on the container does the centring, and leaving `py-*`
183
+ // alive would fight the pinned height under `box-sizing: border-box`.
150
184
  xs: {
151
- container: 'px-2 py-1 text-xs',
185
+ container: 'px-2 text-xs h-6',
152
186
  // `size-3.5` half-step: only icon size aligning with text-xs without
153
187
  // dominating it; codified per CLAUDE.md.
154
188
  arrowIcon: 'size-3.5',
155
189
  },
156
190
  sm: {
157
- container: 'px-2.5 py-1.5 text-sm',
191
+ container: 'px-2.5 text-sm h-8',
158
192
  arrowIcon: 'size-4',
159
193
  },
160
194
  md: {
161
- container: 'px-3 py-2 text-sm',
195
+ container: 'px-3 text-sm h-9',
162
196
  arrowIcon: 'size-4',
163
197
  },
164
198
  lg: {
165
- container: 'px-4 py-2.5 text-base',
199
+ container: 'px-4 text-base h-11',
166
200
  arrowIcon: 'size-5',
167
201
  },
168
202
  xl: {
169
- container: 'px-5 py-3 text-base',
203
+ container: 'px-5 text-base h-12',
170
204
  arrowIcon: 'size-5',
171
205
  },
172
206
  },
@@ -192,18 +226,47 @@ const sortHeaderVariants = tv({
192
226
  },
193
227
  false: {},
194
228
  },
229
+ // True when the consumer mounted the header on an element that is
230
+ // already a control (`<button>`, `<a href>`, `[role="button"]`). The
231
+ // inner container then stays inert, so the canonical focus ring has to
232
+ // move onto the host — it is the element that actually receives focus.
233
+ hostControl: {
234
+ true: {
235
+ host: 'rounded-md cursor-pointer focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500',
236
+ },
237
+ false: {},
238
+ },
195
239
  },
196
240
  defaultVariants: {
197
241
  size: 'md',
198
242
  active: false,
199
243
  direction: 'none',
200
244
  disabled: false,
245
+ hostControl: false,
201
246
  },
202
247
  }, { twMerge: true });
203
248
  /**
204
249
  * Turns any element (e.g., `<th>`, `<div>`, `<button>`) into a sortable header under a parent
205
250
  * `SortDirective`. Renders the projected label plus an arrow that reflects the current direction.
206
251
  * Triggers a sort cycle on click or Enter/Space.
252
+ *
253
+ * @remarks
254
+ * **Where the control and `aria-sort` live depends on the host element.** Both are decided once,
255
+ * at construction, from the host's tag name and its *static* `role` attribute:
256
+ *
257
+ * | Host | `aria-sort` | Interactive element |
258
+ * |---|---|---|
259
+ * | `<th>` / `[role="columnheader"\|"rowheader"]` | on the host | inner container (`role="button"`) |
260
+ * | `<span>` / `<div>` (no role) | **not emitted** | inner container (`role="button"`) |
261
+ * | `<button>` / `<a href>` / `[role="button"\|"link"]` | **not emitted** | the host itself |
262
+ *
263
+ * `aria-sort` is only valid on a `columnheader` / `rowheader`, so emitting it on a `<span>` or
264
+ * `<button>` is an axe `aria-allowed-attr` failure and is silently ignored by assistive tech. When
265
+ * the header is composed *inside* a header cell — `<th><span tw-sort-header>…</span></th>`, the
266
+ * shape `tw-table` generates — the `<th>` owns `aria-sort` (see `tw-column`'s `sortState` input,
267
+ * which auto-resolves against the same `[twSort]` directive). Outside a table there is no
268
+ * `columnheader` to carry the state; the header remains a labelled button and the sort direction
269
+ * is conveyed by the arrow plus the consumer's own live region.
207
270
  */
208
271
  // Selector is deliberately kebab-case to read naturally as an HTML attribute on
209
272
  // host elements (e.g. `<th tw-sort-header>`). Camelcasing it to `twSortHeader`
@@ -235,6 +298,15 @@ class SortHeaderComponent {
235
298
  parent = inject(SortDirective, { optional: true });
236
299
  focusMonitor = inject(FocusMonitor);
237
300
  ariaDescriber = inject(AriaDescriber, { optional: true });
301
+ hostEl = inject(ElementRef).nativeElement;
302
+ /**
303
+ * Whether the host element can legally carry `aria-sort`. Resolved once, at construction — the
304
+ * host's tag name and static attributes are already applied by then, and a later `[attr.role]`
305
+ * binding would make this timing-dependent.
306
+ */
307
+ hostIsHeaderCell = isHeaderCell(this.hostEl);
308
+ /** Whether the host is already a control, in which case the inner container must stay inert. */
309
+ hostIsControl = isInteractiveHost(this.hostEl);
238
310
  containerRef = viewChild('container', /* @ts-ignore */
239
311
  ...(ngDevMode ? [{ debugName: "containerRef" }] : /* istanbul ignore next */ []));
240
312
  _describedBy = null;
@@ -249,7 +321,7 @@ class SortHeaderComponent {
249
321
  // silently ignore subsequent rebinds.
250
322
  effect(() => {
251
323
  const description = this.sortActionDescription();
252
- const el = this.containerRef()?.nativeElement;
324
+ const el = this.controlElement();
253
325
  if (!el || !this.ariaDescriber)
254
326
  return;
255
327
  if (this._describedBy) {
@@ -279,15 +351,37 @@ class SortHeaderComponent {
279
351
  return 'none';
280
352
  }, /* @ts-ignore */
281
353
  ...(ngDevMode ? [{ debugName: "ariaSort" }] : /* istanbul ignore next */ []));
354
+ /**
355
+ * @internal Value actually written to the host's `aria-sort` attribute. `null` — i.e. no
356
+ * attribute — unless the host is a `columnheader` / `rowheader`; see the class remarks.
357
+ */
358
+ hostAriaSort = computed(() => this.hostIsHeaderCell ? this.ariaSort() : null, /* @ts-ignore */
359
+ ...(ngDevMode ? [{ debugName: "hostAriaSort" }] : /* istanbul ignore next */ []));
360
+ /** @internal `role` for the inner container: `'button'` unless the host is already the control. */
361
+ controlRole = computed(() => this.hostIsControl || this.isDisabled() ? null : 'button', /* @ts-ignore */
362
+ ...(ngDevMode ? [{ debugName: "controlRole" }] : /* istanbul ignore next */ []));
363
+ /** @internal `tabindex` for the inner container. Mirrors `controlRole()`. */
364
+ controlTabIndex = computed(() => this.hostIsControl || this.isDisabled() ? null : 0, /* @ts-ignore */
365
+ ...(ngDevMode ? [{ debugName: "controlTabIndex" }] : /* istanbul ignore next */ []));
282
366
  /** Whether the arrow element should render. Active headers always render; inactive render when not disabled so hover reveals the affordance. */
283
367
  renderArrow = computed(() => this.isActive() || !this.isDisabled(), /* @ts-ignore */
284
368
  ...(ngDevMode ? [{ debugName: "renderArrow" }] : /* istanbul ignore next */ []));
369
+ /**
370
+ * The element that carries focus and the accessible description — the host when the consumer
371
+ * mounted the header on a control, the inner container otherwise.
372
+ */
373
+ controlElement() {
374
+ return this.hostIsControl
375
+ ? this.hostEl
376
+ : this.containerRef()?.nativeElement;
377
+ }
285
378
  // ── Variant class computation ──
286
379
  variantResult = computed(() => sortHeaderVariants({
287
380
  size: this.size(),
288
381
  active: this.isActive(),
289
382
  direction: this.effectiveDirection() ?? 'none',
290
383
  disabled: this.isDisabled(),
384
+ hostControl: this.hostIsControl,
291
385
  }), /* @ts-ignore */
292
386
  ...(ngDevMode ? [{ debugName: "variantResult" }] : /* istanbul ignore next */ []));
293
387
  /** @internal */
@@ -315,13 +409,13 @@ class SortHeaderComponent {
315
409
  this.parent?.register(this.id());
316
410
  }
317
411
  ngAfterViewInit() {
318
- const el = this.containerRef()?.nativeElement;
412
+ const el = this.controlElement();
319
413
  if (el) {
320
414
  this.focusMonitor.monitor(el, true);
321
415
  }
322
416
  }
323
417
  ngOnDestroy() {
324
- const el = this.containerRef()?.nativeElement;
418
+ const el = this.controlElement();
325
419
  if (el) {
326
420
  this.focusMonitor.stopMonitoring(el);
327
421
  if (this._describedBy && this.ariaDescriber) {
@@ -347,17 +441,28 @@ class SortHeaderComponent {
347
441
  }
348
442
  }
349
443
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: SortHeaderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
350
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: SortHeaderComponent, isStandalone: true, selector: "[tw-sort-header]", inputs: { id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: true, transformFunction: null }, start: { classPropertyName: "start", publicName: "start", isSignal: true, isRequired: false, transformFunction: null }, disableClear: { classPropertyName: "disableClear", publicName: "disableClear", isSignal: true, isRequired: false, transformFunction: null }, headerDisabled: { classPropertyName: "headerDisabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, arrowPosition: { classPropertyName: "arrowPosition", publicName: "arrowPosition", isSignal: true, isRequired: false, transformFunction: null }, color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, sortActionDescription: { classPropertyName: "sortActionDescription", publicName: "sortActionDescription", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "click": "handleClick()", "keydown": "handleKeydown($event)" }, properties: { "class": "hostClasses()", "attr.aria-sort": "ariaSort()", "attr.aria-disabled": "isDisabled() ? \"true\" : null" } }, viewQueries: [{ propertyName: "containerRef", first: true, predicate: ["container"], descendants: true, isSignal: true }], exportAs: ["twSortHeader"], ngImport: i0, template: "<div\n #container\n [class]=\"containerClasses()\"\n [attr.role]=\"isDisabled() ? null : 'button'\"\n [attr.tabindex]=\"isDisabled() ? null : 0\"\n>\n @if (arrowPosition() === 'before' && renderArrow()) {\n <ng-container *ngTemplateOutlet=\"arrow\" />\n }\n <span [class]=\"labelClasses()\"><ng-content /></span>\n @if (arrowPosition() === 'after' && renderArrow()) {\n <ng-container *ngTemplateOutlet=\"arrow\" />\n }\n</div>\n\n<ng-template #arrow>\n <span [class]=\"arrowClasses()\" data-tw-sort-arrow>\n <ng-content select=\"[twSortHeaderIcon]\">\n <svg\n viewBox=\"0 0 20 20\"\n fill=\"currentColor\"\n aria-hidden=\"true\"\n [class]=\"arrowIconClasses()\"\n >\n <path\n fill-rule=\"evenodd\"\n d=\"M5.22 8.22a.75.75 0 0 1 1.06 0L10 11.94l3.72-3.72a.75.75 0 1 1 1.06 1.06l-4.25 4.25a.75.75 0 0 1-1.06 0L5.22 9.28a.75.75 0 0 1 0-1.06Z\"\n clip-rule=\"evenodd\"\n />\n </svg>\n </ng-content>\n </span>\n</ng-template>\n", dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
444
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: SortHeaderComponent, isStandalone: true, selector: "[tw-sort-header]", inputs: { id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: true, transformFunction: null }, start: { classPropertyName: "start", publicName: "start", isSignal: true, isRequired: false, transformFunction: null }, disableClear: { classPropertyName: "disableClear", publicName: "disableClear", isSignal: true, isRequired: false, transformFunction: null }, headerDisabled: { classPropertyName: "headerDisabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, arrowPosition: { classPropertyName: "arrowPosition", publicName: "arrowPosition", isSignal: true, isRequired: false, transformFunction: null }, color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, sortActionDescription: { classPropertyName: "sortActionDescription", publicName: "sortActionDescription", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "click": "handleClick()", "keydown": "handleKeydown($event)" }, properties: { "class": "hostClasses()", "attr.aria-sort": "hostAriaSort()", "attr.data-sort-direction": "ariaSort()", "attr.aria-disabled": "isDisabled() ? \"true\" : null" } }, viewQueries: [{ propertyName: "containerRef", first: true, predicate: ["container"], descendants: true, isSignal: true }], exportAs: ["twSortHeader"], ngImport: i0, template: "<!--\n The container is the interactive control (role=button + tabindex) unless the\n consumer already mounted the header on one - a button, an anchor, or an\n element carrying a static button/link role. Nesting a second widget inside\n such a host is an axe nested-interactive failure and leaves assistive tech\n with two competing activation targets, so controlRole and controlTabIndex\n both resolve to null there and the host keeps the focus ring instead.\n-->\n<div\n #container\n [class]=\"containerClasses()\"\n [attr.role]=\"controlRole()\"\n [attr.tabindex]=\"controlTabIndex()\"\n>\n @if (arrowPosition() === 'before' && renderArrow()) {\n <ng-container *ngTemplateOutlet=\"arrow\" />\n }\n <span [class]=\"labelClasses()\"><ng-content /></span>\n @if (arrowPosition() === 'after' && renderArrow()) {\n <ng-container *ngTemplateOutlet=\"arrow\" />\n }\n</div>\n\n<ng-template #arrow>\n <span [class]=\"arrowClasses()\" data-tw-sort-arrow>\n <ng-content select=\"[twSortHeaderIcon]\">\n <svg\n viewBox=\"0 0 20 20\"\n fill=\"currentColor\"\n aria-hidden=\"true\"\n [class]=\"arrowIconClasses()\"\n >\n <path\n fill-rule=\"evenodd\"\n d=\"M5.22 8.22a.75.75 0 0 1 1.06 0L10 11.94l3.72-3.72a.75.75 0 1 1 1.06 1.06l-4.25 4.25a.75.75 0 0 1-1.06 0L5.22 9.28a.75.75 0 0 1 0-1.06Z\"\n clip-rule=\"evenodd\"\n />\n </svg>\n </ng-content>\n </span>\n</ng-template>\n", dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
351
445
  }
352
446
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: SortHeaderComponent, decorators: [{
353
447
  type: Component,
354
448
  args: [{ selector: '[tw-sort-header]', exportAs: 'twSortHeader', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgTemplateOutlet], host: {
355
449
  '[class]': 'hostClasses()',
356
- '[attr.aria-sort]': 'ariaSort()',
450
+ '[attr.aria-sort]': 'hostAriaSort()',
451
+ // Sort state as a data attribute, on EVERY host shape.
452
+ //
453
+ // `aria-sort` is only legal on a header cell, so on a span or button host
454
+ // it is deliberately absent — which left the sort state with no stable,
455
+ // valid hook at all on those hosts. The direction was readable only from
456
+ // the arrow's rotation utility class, which is implementation detail no
457
+ // test or consumer should depend on. A `data-*` attribute is valid on any
458
+ // element and carries no ARIA semantics, so it states the same thing
459
+ // without lying to assistive tech. Values mirror `aria-sort` exactly, so
460
+ // a reader does not have to learn a second vocabulary.
461
+ '[attr.data-sort-direction]': 'ariaSort()',
357
462
  '[attr.aria-disabled]': 'isDisabled() ? "true" : null',
358
463
  '(click)': 'handleClick()',
359
464
  '(keydown)': 'handleKeydown($event)',
360
- }, template: "<div\n #container\n [class]=\"containerClasses()\"\n [attr.role]=\"isDisabled() ? null : 'button'\"\n [attr.tabindex]=\"isDisabled() ? null : 0\"\n>\n @if (arrowPosition() === 'before' && renderArrow()) {\n <ng-container *ngTemplateOutlet=\"arrow\" />\n }\n <span [class]=\"labelClasses()\"><ng-content /></span>\n @if (arrowPosition() === 'after' && renderArrow()) {\n <ng-container *ngTemplateOutlet=\"arrow\" />\n }\n</div>\n\n<ng-template #arrow>\n <span [class]=\"arrowClasses()\" data-tw-sort-arrow>\n <ng-content select=\"[twSortHeaderIcon]\">\n <svg\n viewBox=\"0 0 20 20\"\n fill=\"currentColor\"\n aria-hidden=\"true\"\n [class]=\"arrowIconClasses()\"\n >\n <path\n fill-rule=\"evenodd\"\n d=\"M5.22 8.22a.75.75 0 0 1 1.06 0L10 11.94l3.72-3.72a.75.75 0 1 1 1.06 1.06l-4.25 4.25a.75.75 0 0 1-1.06 0L5.22 9.28a.75.75 0 0 1 0-1.06Z\"\n clip-rule=\"evenodd\"\n />\n </svg>\n </ng-content>\n </span>\n</ng-template>\n" }]
465
+ }, template: "<!--\n The container is the interactive control (role=button + tabindex) unless the\n consumer already mounted the header on one - a button, an anchor, or an\n element carrying a static button/link role. Nesting a second widget inside\n such a host is an axe nested-interactive failure and leaves assistive tech\n with two competing activation targets, so controlRole and controlTabIndex\n both resolve to null there and the host keeps the focus ring instead.\n-->\n<div\n #container\n [class]=\"containerClasses()\"\n [attr.role]=\"controlRole()\"\n [attr.tabindex]=\"controlTabIndex()\"\n>\n @if (arrowPosition() === 'before' && renderArrow()) {\n <ng-container *ngTemplateOutlet=\"arrow\" />\n }\n <span [class]=\"labelClasses()\"><ng-content /></span>\n @if (arrowPosition() === 'after' && renderArrow()) {\n <ng-container *ngTemplateOutlet=\"arrow\" />\n }\n</div>\n\n<ng-template #arrow>\n <span [class]=\"arrowClasses()\" data-tw-sort-arrow>\n <ng-content select=\"[twSortHeaderIcon]\">\n <svg\n viewBox=\"0 0 20 20\"\n fill=\"currentColor\"\n aria-hidden=\"true\"\n [class]=\"arrowIconClasses()\"\n >\n <path\n fill-rule=\"evenodd\"\n d=\"M5.22 8.22a.75.75 0 0 1 1.06 0L10 11.94l3.72-3.72a.75.75 0 1 1 1.06 1.06l-4.25 4.25a.75.75 0 0 1-1.06 0L5.22 9.28a.75.75 0 0 1 0-1.06Z\"\n clip-rule=\"evenodd\"\n />\n </svg>\n </ng-content>\n </span>\n</ng-template>\n" }]
361
466
  }], ctorParameters: () => [], propDecorators: { id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: true }] }], start: [{ type: i0.Input, args: [{ isSignal: true, alias: "start", required: false }] }], disableClear: [{ type: i0.Input, args: [{ isSignal: true, alias: "disableClear", required: false }] }], headerDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], arrowPosition: [{ type: i0.Input, args: [{ isSignal: true, alias: "arrowPosition", required: false }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], sortActionDescription: [{ type: i0.Input, args: [{ isSignal: true, alias: "sortActionDescription", required: false }] }], containerRef: [{ type: i0.ViewChild, args: ['container', { isSignal: true }] }] } });
362
467
 
363
468
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"cdevhub-ngx-tw-sort.mjs","sources":["../../../projects/ngx-tw/sort/sort.ts","../../../projects/ngx-tw/sort/sort-header.ts","../../../projects/ngx-tw/sort/sort-header.html","../../../projects/ngx-tw/sort/cdevhub-ngx-tw-sort.ts"],"sourcesContent":["import {\n Directive,\n input,\n isDevMode,\n model,\n output,\n} from '@angular/core';\nimport { TW_SORT_HANDLE } from '@cdevhub/ngx-tw/core';\n\n/** Sort direction. `null` represents the cleared (unsorted) state. */\nexport type SortDirection = 'asc' | 'desc' | null;\n\n/** Shape that `SortHeaderComponent` implements when registering with a parent `SortDirective`. */\nexport interface TwSortable {\n /** Unique id identifying the column or field this header sorts. */\n readonly id: string;\n /** Header-level starting direction. `undefined` falls back to the parent directive's `start`. */\n readonly start: 'asc' | 'desc' | undefined;\n /** Header-level override for disabling the cleared state in the cycle. `undefined` falls back to the parent directive's `disableClear`. */\n readonly disableClear: boolean | undefined;\n /** Whether this header is disabled. */\n readonly disabled: boolean;\n}\n\n/** Payload emitted by `SortDirective.sortChange` whenever a user interaction changes the sort state. */\nexport interface TwSortEvent {\n /** The new active header id, or `null` when the sort was cleared. */\n active: string | null;\n /** The new sort direction. */\n direction: SortDirection;\n /** Snapshot of the previous state, before this change. */\n previous: {\n /** Previous active header id. */\n active: string | null;\n /** Previous sort direction. */\n direction: SortDirection;\n };\n}\n\n/**\n * Returns the direction cycle for a given starting direction and disable-clear flag.\n * @internal Exported for unit tests; not part of the public API.\n */\nexport function getSortDirectionCycle(\n start: 'asc' | 'desc',\n disableClear: boolean,\n): SortDirection[] {\n const cycle: SortDirection[] = start === 'desc' ? ['desc', 'asc'] : ['asc', 'desc'];\n if (!disableClear) cycle.push(null);\n return cycle;\n}\n\n/**\n * Container directive that holds the current sort state (`active` id + `direction`) and\n * coordinates with child `SortHeaderComponent` instances. Compose with any rendering layer —\n * tables, lists, custom grids — by placing child headers inside an element marked `[twSort]`.\n *\n * @remarks\n * **`tw*` input/output aliases.** Every `input()` / `output()` / `model()` on this directive\n * carries a `tw*` alias (e.g. `'twSortActive'`, `'twSortDirection'`, `'twSortChange'`). The\n * aliases namespace bindings under the directive selector so they cannot collide with\n * attributes the host element already owns (a `<table>` consumer binding `[active]` would\n * otherwise be ambiguous). This mirrors Angular Material's `mat*` aliasing precedent on\n * `MatSort`. Compodoc surfaces both the field name and the alias; consumer templates MUST\n * bind via the aliased name (`[twSortActive]`, `(twSortChange)`, …). Removing an alias is a\n * breaking API change requiring a major version bump.\n */\n@Directive({\n selector: '[twSort]',\n exportAs: 'twSort',\n host: {\n 'class': 'tw-sort',\n },\n providers: [{ provide: TW_SORT_HANDLE, useExisting: SortDirective }],\n})\nexport class SortDirective {\n /** The id of the currently sorted header, or `null` when nothing is sorted. Two-way bindable via `[(twSortActive)]`. Defaults to `null`. */\n readonly active = model<string | null>(null, { alias: 'twSortActive' });\n\n /** Current sort direction. `null` represents the cleared state. Two-way bindable via `[(twSortDirection)]`. Defaults to `null`. */\n readonly direction = model<SortDirection>(null, { alias: 'twSortDirection' });\n\n /** Starting direction used when a header becomes active for the first time. Per-header `start` overrides this. Defaults to `'asc'`. */\n readonly start = input<'asc' | 'desc'>('asc', { alias: 'twSortStart' });\n\n /** When true, the direction cycle skips the cleared (`null`) state — headers toggle between `'asc'` and `'desc'` only. Per-header `disableClear` overrides this. Defaults to `false`. */\n readonly disableClear = input<boolean>(false, { alias: 'twSortDisableClear' });\n\n /** When true, all child sort headers are disabled. Defaults to `false`. */\n readonly disabled = input<boolean>(false, { alias: 'twSortDisabled' });\n\n /** Fires whenever the user changes `active` or `direction` by interacting with a header. Programmatic writes to `[(twSortActive)]` / `[(twSortDirection)]` do NOT emit. */\n readonly sortChange = output<TwSortEvent>({ alias: 'twSortChange' });\n\n private readonly sortableIds = new Set<string>();\n\n /** Registers a header id so duplicates can be detected. Called by `SortHeaderComponent` on init. Throws in dev mode if another header with the same id is already registered. */\n register(id: string): void {\n if (isDevMode()) {\n if (!id) {\n throw new Error('tw-sort-header must be provided with a unique id.');\n }\n if (this.sortableIds.has(id)) {\n throw new Error(\n `Cannot have two tw-sort-header elements with the same id (${id}).`,\n );\n }\n }\n this.sortableIds.add(id);\n }\n\n /** Deregisters a header id. Called on destroy. */\n deregister(id: string): void {\n this.sortableIds.delete(id);\n }\n\n /** Cycles the direction for the given header and emits `sortChange`. No-op when the directive or the header is disabled. */\n sort(sortable: TwSortable): void {\n if (this.disabled() || sortable.disabled) return;\n\n const previousActive = this.active();\n const previousDirection = this.direction();\n\n let nextActive: string | null;\n let nextDirection: SortDirection;\n\n if (previousActive !== sortable.id) {\n nextActive = sortable.id;\n nextDirection = sortable.start ?? this.start();\n } else {\n nextDirection = this.getNextSortDirection(sortable);\n nextActive = nextDirection === null ? null : sortable.id;\n }\n\n this.active.set(nextActive);\n this.direction.set(nextDirection);\n\n this.sortChange.emit({\n active: nextActive,\n direction: nextDirection,\n previous: {\n active: previousActive,\n direction: previousDirection,\n },\n });\n }\n\n /** Returns the next direction in the cycle for the given header, based on the current state and header/parent overrides. */\n getNextSortDirection(sortable: TwSortable): SortDirection {\n const start = sortable.start ?? this.start();\n const disableClear = sortable.disableClear ?? this.disableClear();\n const cycle = getSortDirectionCycle(start, disableClear);\n\n // If the header is not currently active, the next direction is the starting one.\n if (this.active() !== sortable.id) {\n return start;\n }\n\n const currentIndex = cycle.indexOf(this.direction());\n if (currentIndex < 0) {\n return start;\n }\n return cycle[(currentIndex + 1) % cycle.length] as SortDirection;\n }\n}\n","import {\n type AfterViewInit,\n ChangeDetectionStrategy,\n Component,\n computed,\n effect,\n type ElementRef,\n inject,\n input,\n isDevMode,\n type OnDestroy,\n type OnInit,\n viewChild,\n} from '@angular/core';\nimport { NgTemplateOutlet } from '@angular/common';\nimport { AriaDescriber, FocusMonitor } from '@angular/cdk/a11y';\nimport { tv } from 'tailwind-variants';\nimport type { TwColor, TwSize } from '@cdevhub/ngx-tw/core';\nimport { SortDirective, type SortDirection, type TwSortable } from './sort';\n\nfunction snapshot(self: SortHeaderComponent): TwSortable {\n return {\n id: self.id(),\n start: self.start(),\n disableClear: self.disableClear(),\n disabled: self.isDisabled(),\n };\n}\n\n/** Position of the sort arrow relative to the header label. */\nexport type TwSortArrowPosition = 'before' | 'after';\n\n// ── Static per-color maps (Tailwind v4 requires literal class strings for its JIT scanner). ──\n\nconst ARROW_ACTIVE_COLOR: Record<TwColor, string> = {\n primary: 'text-primary-600',\n secondary: 'text-secondary-600',\n accent: 'text-accent-600',\n neutral: 'text-fg',\n info: 'text-info-600',\n success: 'text-success-600',\n warning: 'text-warning-600',\n error: 'text-error-600',\n};\n\n// ── tv() config ──\n\nconst sortHeaderVariants = tv(\n {\n slots: {\n host: 'group select-none',\n container:\n 'inline-flex items-center gap-1.5 cursor-pointer rounded-md font-medium text-fg transition-colors duration-normal motion-reduce:transition-none hover:bg-surface-muted focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500',\n label: 'min-w-0',\n arrow:\n 'inline-flex shrink-0 items-center justify-center transition-opacity duration-normal motion-reduce:transition-none',\n arrowIcon:\n 'shrink-0 transition-transform duration-normal motion-reduce:transition-none',\n },\n variants: {\n size: {\n xs: {\n container: 'px-2 py-1 text-xs',\n // `size-3.5` half-step: only icon size aligning with text-xs without\n // dominating it; codified per CLAUDE.md.\n arrowIcon: 'size-3.5',\n },\n sm: {\n container: 'px-2.5 py-1.5 text-sm',\n arrowIcon: 'size-4',\n },\n md: {\n container: 'px-3 py-2 text-sm',\n arrowIcon: 'size-4',\n },\n lg: {\n container: 'px-4 py-2.5 text-base',\n arrowIcon: 'size-5',\n },\n xl: {\n container: 'px-5 py-3 text-base',\n arrowIcon: 'size-5',\n },\n },\n active: {\n true: {\n container: 'text-fg',\n arrow: 'opacity-100',\n },\n false: {\n container: 'text-fg-muted',\n arrow:\n 'opacity-0 group-hover:opacity-50 group-focus-within:opacity-50',\n },\n },\n direction: {\n asc: { arrowIcon: 'rotate-180' },\n desc: { arrowIcon: 'rotate-0' },\n none: { arrowIcon: 'rotate-0' },\n },\n disabled: {\n true: {\n host: 'opacity-50 pointer-events-none',\n container: 'cursor-not-allowed',\n },\n false: {},\n },\n },\n defaultVariants: {\n size: 'md',\n active: false,\n direction: 'none',\n disabled: false,\n },\n },\n { twMerge: true },\n);\n\n/**\n * Turns any element (e.g., `<th>`, `<div>`, `<button>`) into a sortable header under a parent\n * `SortDirective`. Renders the projected label plus an arrow that reflects the current direction.\n * Triggers a sort cycle on click or Enter/Space.\n */\n// Selector is deliberately kebab-case to read naturally as an HTML attribute on\n// host elements (e.g. `<th tw-sort-header>`). Camelcasing it to `twSortHeader`\n// would break the public API and ~20 demo usages.\n@Component({\n // eslint-disable-next-line @angular-eslint/component-selector\n selector: '[tw-sort-header]',\n exportAs: 'twSortHeader',\n changeDetection: ChangeDetectionStrategy.OnPush,\n imports: [NgTemplateOutlet],\n templateUrl: './sort-header.html',\n host: {\n '[class]': 'hostClasses()',\n '[attr.aria-sort]': 'ariaSort()',\n '[attr.aria-disabled]': 'isDisabled() ? \"true\" : null',\n '(click)': 'handleClick()',\n '(keydown)': 'handleKeydown($event)',\n },\n})\nexport class SortHeaderComponent implements OnInit, AfterViewInit, OnDestroy {\n /** Unique id for this header. Required — identifies the field/column this header sorts. */\n readonly id = input.required<string>();\n\n /** Overrides the parent directive's `start` for this header only. `undefined` inherits from the parent. */\n readonly start = input<'asc' | 'desc' | undefined>(undefined);\n\n /** Overrides the parent directive's `disableClear` for this header only. `undefined` inherits from the parent. */\n readonly disableClear = input<boolean | undefined>(undefined);\n\n /** When true, this header is disabled — clicks and keyboard activation are ignored. Defaults to `false`. */\n readonly headerDisabled = input<boolean>(false, { alias: 'disabled' });\n\n /** Whether the sort arrow renders `'before'` or `'after'` the projected label. Defaults to `'after'`. */\n readonly arrowPosition = input<TwSortArrowPosition>('after');\n\n /** Semantic color used to tint the arrow when this header is active. Defaults to `'primary'`. */\n readonly color = input<TwColor>('primary');\n\n /** Controls padding and font size. Uses the shared `TwSize` scale. Defaults to `'md'`. */\n readonly size = input<TwSize>('md');\n\n /** Accessible description applied via `AriaDescriber` so screen readers announce the sort action alongside the header text. Defaults to `'Sort'`. */\n readonly sortActionDescription = input<string>('Sort');\n\n private readonly parent = inject(SortDirective, { optional: true });\n private readonly focusMonitor = inject(FocusMonitor);\n private readonly ariaDescriber = inject(AriaDescriber, { optional: true });\n\n private readonly containerRef =\n viewChild<ElementRef<HTMLElement>>('container');\n\n private _describedBy: string | null = null;\n\n constructor() {\n if (!this.parent && isDevMode()) {\n throw new Error(\n 'tw-sort-header must be placed within a parent element with the twSort directive.',\n );\n }\n\n // Reactive: re-tags the host whenever `sortActionDescription` (a public\n // signal input) changes; consumers may swap the description at runtime for\n // i18n / state-driven copy. Cleanup runs in `ngOnDestroy`. Not a one-shot\n // `afterNextRender`: that would freeze the description at first render and\n // silently ignore subsequent rebinds.\n effect(() => {\n const description = this.sortActionDescription();\n const el = this.containerRef()?.nativeElement;\n if (!el || !this.ariaDescriber) return;\n if (this._describedBy) {\n this.ariaDescriber.removeDescription(el, this._describedBy);\n }\n this.ariaDescriber.describe(el, description);\n this._describedBy = description;\n });\n }\n\n // ── Derived signals ──\n\n /** Whether this header is currently the active (sorted) header. */\n readonly isActive = computed(\n () => this.parent?.active() === this.id(),\n );\n\n /** Whether this header is disabled via its own input or the parent directive's `disabled`. */\n readonly isDisabled = computed(\n () => this.headerDisabled() || (this.parent?.disabled() ?? false),\n );\n\n /** Active direction for *this* header (null when not active). */\n readonly effectiveDirection = computed<SortDirection>(() =>\n this.isActive() ? (this.parent?.direction() ?? null) : null,\n );\n\n /** `aria-sort` value: `'ascending' | 'descending' | 'none'`. */\n readonly ariaSort = computed(() => {\n const d = this.effectiveDirection();\n if (d === 'asc') return 'ascending';\n if (d === 'desc') return 'descending';\n return 'none';\n });\n\n /** Whether the arrow element should render. Active headers always render; inactive render when not disabled so hover reveals the affordance. */\n readonly renderArrow = computed(() => this.isActive() || !this.isDisabled());\n\n // ── Variant class computation ──\n\n private readonly variantResult = computed(() =>\n sortHeaderVariants({\n size: this.size(),\n active: this.isActive(),\n direction: this.effectiveDirection() ?? 'none',\n disabled: this.isDisabled(),\n }),\n );\n\n /** @internal */\n readonly hostClasses = computed(() => this.variantResult().host());\n\n /** @internal */\n readonly containerClasses = computed(() => this.variantResult().container());\n\n /** @internal */\n readonly labelClasses = computed(() => this.variantResult().label());\n\n /** @internal */\n readonly arrowClasses = computed(() => {\n const base = this.variantResult().arrow();\n if (!this.isActive()) return base;\n return `${base} ${ARROW_ACTIVE_COLOR[this.color()]}`;\n });\n\n /** @internal */\n readonly arrowIconClasses = computed(() => this.variantResult().arrowIcon());\n\n // ── Lifecycle ──\n\n ngOnInit(): void {\n this.parent?.register(this.id());\n }\n\n ngAfterViewInit(): void {\n const el = this.containerRef()?.nativeElement;\n if (el) {\n this.focusMonitor.monitor(el, true);\n }\n }\n\n ngOnDestroy(): void {\n const el = this.containerRef()?.nativeElement;\n if (el) {\n this.focusMonitor.stopMonitoring(el);\n if (this._describedBy && this.ariaDescriber) {\n this.ariaDescriber.removeDescription(el, this._describedBy);\n }\n }\n this.parent?.deregister(this.id());\n }\n\n // ── Event handlers ──\n\n /** @internal */\n handleClick(): void {\n if (this.isDisabled()) return;\n this.parent?.sort(snapshot(this));\n }\n\n /** @internal */\n handleKeydown(event: KeyboardEvent): void {\n if (this.isDisabled()) return;\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault();\n this.parent?.sort(snapshot(this));\n }\n }\n}\n","<div\n #container\n [class]=\"containerClasses()\"\n [attr.role]=\"isDisabled() ? null : 'button'\"\n [attr.tabindex]=\"isDisabled() ? null : 0\"\n>\n @if (arrowPosition() === 'before' && renderArrow()) {\n <ng-container *ngTemplateOutlet=\"arrow\" />\n }\n <span [class]=\"labelClasses()\"><ng-content /></span>\n @if (arrowPosition() === 'after' && renderArrow()) {\n <ng-container *ngTemplateOutlet=\"arrow\" />\n }\n</div>\n\n<ng-template #arrow>\n <span [class]=\"arrowClasses()\" data-tw-sort-arrow>\n <ng-content select=\"[twSortHeaderIcon]\">\n <svg\n viewBox=\"0 0 20 20\"\n fill=\"currentColor\"\n aria-hidden=\"true\"\n [class]=\"arrowIconClasses()\"\n >\n <path\n fill-rule=\"evenodd\"\n d=\"M5.22 8.22a.75.75 0 0 1 1.06 0L10 11.94l3.72-3.72a.75.75 0 1 1 1.06 1.06l-4.25 4.25a.75.75 0 0 1-1.06 0L5.22 9.28a.75.75 0 0 1 0-1.06Z\"\n clip-rule=\"evenodd\"\n />\n </svg>\n </ng-content>\n </span>\n</ng-template>\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"cdevhub-ngx-tw-sort.mjs","sources":["../../../projects/ngx-tw/sort/sort.ts","../../../projects/ngx-tw/sort/sort-header.ts","../../../projects/ngx-tw/sort/sort-header.html","../../../projects/ngx-tw/sort/cdevhub-ngx-tw-sort.ts"],"sourcesContent":["import {\n Directive,\n input,\n isDevMode,\n model,\n output,\n} from '@angular/core';\nimport { TW_SORT_HANDLE } from '@cdevhub/ngx-tw/core';\n\n/** Sort direction. `null` represents the cleared (unsorted) state. */\nexport type SortDirection = 'asc' | 'desc' | null;\n\n/** Shape that `SortHeaderComponent` implements when registering with a parent `SortDirective`. */\nexport interface TwSortable {\n /** Unique id identifying the column or field this header sorts. */\n readonly id: string;\n /** Header-level starting direction. `undefined` falls back to the parent directive's `start`. */\n readonly start: 'asc' | 'desc' | undefined;\n /** Header-level override for disabling the cleared state in the cycle. `undefined` falls back to the parent directive's `disableClear`. */\n readonly disableClear: boolean | undefined;\n /** Whether this header is disabled. */\n readonly disabled: boolean;\n}\n\n/** Payload emitted by `SortDirective.sortChange` whenever a user interaction changes the sort state. */\nexport interface TwSortEvent {\n /** The new active header id, or `null` when the sort was cleared. */\n active: string | null;\n /** The new sort direction. */\n direction: SortDirection;\n /** Snapshot of the previous state, before this change. */\n previous: {\n /** Previous active header id. */\n active: string | null;\n /** Previous sort direction. */\n direction: SortDirection;\n };\n}\n\n/**\n * Returns the direction cycle for a given starting direction and disable-clear flag.\n * @internal Exported for unit tests; not part of the public API.\n */\nexport function getSortDirectionCycle(\n start: 'asc' | 'desc',\n disableClear: boolean,\n): SortDirection[] {\n const cycle: SortDirection[] = start === 'desc' ? ['desc', 'asc'] : ['asc', 'desc'];\n if (!disableClear) cycle.push(null);\n return cycle;\n}\n\n/**\n * Container directive that holds the current sort state (`active` id + `direction`) and\n * coordinates with child `SortHeaderComponent` instances. Compose with any rendering layer —\n * tables, lists, custom grids — by placing child headers inside an element marked `[twSort]`.\n *\n * @remarks\n * **`tw*` input/output aliases.** Every `input()` / `output()` / `model()` on this directive\n * carries a `tw*` alias (e.g. `'twSortActive'`, `'twSortDirection'`, `'twSortChange'`). The\n * aliases namespace bindings under the directive selector so they cannot collide with\n * attributes the host element already owns (a `<table>` consumer binding `[active]` would\n * otherwise be ambiguous). This mirrors Angular Material's `mat*` aliasing precedent on\n * `MatSort`. Compodoc surfaces both the field name and the alias; consumer templates MUST\n * bind via the aliased name (`[twSortActive]`, `(twSortChange)`, …). Removing an alias is a\n * breaking API change requiring a major version bump.\n */\n@Directive({\n selector: '[twSort]',\n exportAs: 'twSort',\n host: {\n 'class': 'tw-sort',\n },\n providers: [{ provide: TW_SORT_HANDLE, useExisting: SortDirective }],\n})\nexport class SortDirective {\n /** The id of the currently sorted header, or `null` when nothing is sorted. Two-way bindable via `[(twSortActive)]`. Defaults to `null`. */\n readonly active = model<string | null>(null, { alias: 'twSortActive' });\n\n /** Current sort direction. `null` represents the cleared state. Two-way bindable via `[(twSortDirection)]`. Defaults to `null`. */\n readonly direction = model<SortDirection>(null, { alias: 'twSortDirection' });\n\n /** Starting direction used when a header becomes active for the first time. Per-header `start` overrides this. Defaults to `'asc'`. Alias: `twSortStart`. */\n readonly start = input<'asc' | 'desc'>('asc', { alias: 'twSortStart' });\n\n /** When true, the direction cycle skips the cleared (`null`) state — headers toggle between `'asc'` and `'desc'` only. Per-header `disableClear` overrides this. Defaults to `false`. Alias: `twSortDisableClear`. */\n readonly disableClear = input<boolean>(false, { alias: 'twSortDisableClear' });\n\n /** When true, all child sort headers are disabled. Defaults to `false`. Alias: `twSortDisabled`. */\n readonly disabled = input<boolean>(false, { alias: 'twSortDisabled' });\n\n /** Fires whenever the user changes `active` or `direction` by interacting with a header. Programmatic writes to `[(twSortActive)]` / `[(twSortDirection)]` do NOT emit. Alias: `twSortChange`. */\n readonly sortChange = output<TwSortEvent>({ alias: 'twSortChange' });\n\n private readonly sortableIds = new Set<string>();\n\n /** Registers a header id so duplicates can be detected. Called by `SortHeaderComponent` on init. Throws in dev mode if another header with the same id is already registered. */\n register(id: string): void {\n if (isDevMode()) {\n if (!id) {\n throw new Error('tw-sort-header must be provided with a unique id.');\n }\n if (this.sortableIds.has(id)) {\n throw new Error(\n `Cannot have two tw-sort-header elements with the same id (${id}).`,\n );\n }\n }\n this.sortableIds.add(id);\n }\n\n /** Deregisters a header id. Called on destroy. */\n deregister(id: string): void {\n this.sortableIds.delete(id);\n }\n\n /** Cycles the direction for the given header and emits `sortChange`. No-op when the directive or the header is disabled. */\n sort(sortable: TwSortable): void {\n if (this.disabled() || sortable.disabled) return;\n\n const previousActive = this.active();\n const previousDirection = this.direction();\n\n let nextActive: string | null;\n let nextDirection: SortDirection;\n\n if (previousActive !== sortable.id) {\n nextActive = sortable.id;\n nextDirection = sortable.start ?? this.start();\n } else {\n nextDirection = this.getNextSortDirection(sortable);\n nextActive = nextDirection === null ? null : sortable.id;\n }\n\n this.active.set(nextActive);\n this.direction.set(nextDirection);\n\n this.sortChange.emit({\n active: nextActive,\n direction: nextDirection,\n previous: {\n active: previousActive,\n direction: previousDirection,\n },\n });\n }\n\n /** Returns the next direction in the cycle for the given header, based on the current state and header/parent overrides. */\n getNextSortDirection(sortable: TwSortable): SortDirection {\n const start = sortable.start ?? this.start();\n const disableClear = sortable.disableClear ?? this.disableClear();\n const cycle = getSortDirectionCycle(start, disableClear);\n\n // If the header is not currently active, the next direction is the starting one.\n if (this.active() !== sortable.id) {\n return start;\n }\n\n const currentIndex = cycle.indexOf(this.direction());\n if (currentIndex < 0) {\n return start;\n }\n return cycle[(currentIndex + 1) % cycle.length] as SortDirection;\n }\n}\n","import {\n type AfterViewInit,\n ChangeDetectionStrategy,\n Component,\n computed,\n effect,\n ElementRef,\n inject,\n input,\n isDevMode,\n type OnDestroy,\n type OnInit,\n viewChild,\n} from '@angular/core';\nimport { NgTemplateOutlet } from '@angular/common';\nimport { AriaDescriber, FocusMonitor } from '@angular/cdk/a11y';\nimport { tv } from 'tailwind-variants';\nimport type { TwColor, TwSize } from '@cdevhub/ngx-tw/core';\nimport { SortDirective, type SortDirection, type TwSortable } from './sort';\n\nfunction snapshot(self: SortHeaderComponent): TwSortable {\n return {\n id: self.id(),\n start: self.start(),\n disableClear: self.disableClear(),\n disabled: self.isDisabled(),\n };\n}\n\n/** Position of the sort arrow relative to the header label. */\nexport type TwSortArrowPosition = 'before' | 'after';\n\n/**\n * Whether the host element exposes a `columnheader` / `rowheader` role, which are the only two\n * roles `aria-sort` is valid on. A static `role` attribute wins over the tag name; a `[attr.role]`\n * *binding* is not visible yet at construction time and is deliberately not consulted.\n */\nfunction isHeaderCell(el: HTMLElement): boolean {\n const role = el.getAttribute('role');\n if (role !== null) return role === 'columnheader' || role === 'rowheader';\n return el.tagName === 'TH';\n}\n\n/**\n * Whether the host element is already an interactive control. When it is, the inner container must\n * NOT also become one — two nested widgets is an axe `nested-interactive` failure and leaves\n * assistive tech with an ambiguous activation target.\n */\nfunction isInteractiveHost(el: HTMLElement): boolean {\n const role = el.getAttribute('role');\n if (role === 'button' || role === 'link') return true;\n const tag = el.tagName;\n return tag === 'BUTTON' || (tag === 'A' && el.hasAttribute('href'));\n}\n\n// ── Static per-color maps (Tailwind v4 requires literal class strings for its JIT scanner). ──\n\nconst ARROW_ACTIVE_COLOR: Record<TwColor, string> = {\n primary: 'text-primary-600',\n secondary: 'text-secondary-600',\n accent: 'text-accent-600',\n neutral: 'text-fg',\n info: 'text-info-600',\n success: 'text-success-600',\n warning: 'text-warning-600',\n error: 'text-error-600',\n};\n\n// ── tv() config ──\n\nconst sortHeaderVariants = tv(\n {\n slots: {\n host: 'group select-none',\n // `align-top` is load-bearing, not cosmetic. The container is an\n // inline-level box inside the host block, so without it the host's line\n // box is `strut ∪ baseline-aligned container` — at `xs` the strut's\n // descent hangs 2px below the 24px control, and the row measures 26px.\n // `vertical-align: top` flushes the box to the line-box top, making the\n // host exactly the pinned height at every size. Inert at sm–xl, where the\n // control already exceeds the strut.\n container:\n 'inline-flex align-top items-center gap-1.5 cursor-pointer rounded-md font-medium text-fg transition-colors duration-normal motion-reduce:transition-none hover:bg-surface-muted focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500',\n label: 'min-w-0',\n arrow:\n 'inline-flex shrink-0 items-center justify-center transition-opacity duration-normal motion-reduce:transition-none',\n arrowIcon:\n 'shrink-0 transition-transform duration-normal motion-reduce:transition-none',\n },\n variants: {\n size: {\n // Heights are pinned to the control scale (docs/vertical-rhythm.md):\n // 24 / 32 / 36 / 44 / 48. Vertical padding is deleted deliberately —\n // `items-center` on the container does the centring, and leaving `py-*`\n // alive would fight the pinned height under `box-sizing: border-box`.\n xs: {\n container: 'px-2 text-xs h-6',\n // `size-3.5` half-step: only icon size aligning with text-xs without\n // dominating it; codified per CLAUDE.md.\n arrowIcon: 'size-3.5',\n },\n sm: {\n container: 'px-2.5 text-sm h-8',\n arrowIcon: 'size-4',\n },\n md: {\n container: 'px-3 text-sm h-9',\n arrowIcon: 'size-4',\n },\n lg: {\n container: 'px-4 text-base h-11',\n arrowIcon: 'size-5',\n },\n xl: {\n container: 'px-5 text-base h-12',\n arrowIcon: 'size-5',\n },\n },\n active: {\n true: {\n container: 'text-fg',\n arrow: 'opacity-100',\n },\n false: {\n container: 'text-fg-muted',\n arrow:\n 'opacity-0 group-hover:opacity-50 group-focus-within:opacity-50',\n },\n },\n direction: {\n asc: { arrowIcon: 'rotate-180' },\n desc: { arrowIcon: 'rotate-0' },\n none: { arrowIcon: 'rotate-0' },\n },\n disabled: {\n true: {\n host: 'opacity-50 pointer-events-none',\n container: 'cursor-not-allowed',\n },\n false: {},\n },\n // True when the consumer mounted the header on an element that is\n // already a control (`<button>`, `<a href>`, `[role=\"button\"]`). The\n // inner container then stays inert, so the canonical focus ring has to\n // move onto the host — it is the element that actually receives focus.\n hostControl: {\n true: {\n host: 'rounded-md cursor-pointer focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500',\n },\n false: {},\n },\n },\n defaultVariants: {\n size: 'md',\n active: false,\n direction: 'none',\n disabled: false,\n hostControl: false,\n },\n },\n { twMerge: true },\n);\n\n/**\n * Turns any element (e.g., `<th>`, `<div>`, `<button>`) into a sortable header under a parent\n * `SortDirective`. Renders the projected label plus an arrow that reflects the current direction.\n * Triggers a sort cycle on click or Enter/Space.\n *\n * @remarks\n * **Where the control and `aria-sort` live depends on the host element.** Both are decided once,\n * at construction, from the host's tag name and its *static* `role` attribute:\n *\n * | Host | `aria-sort` | Interactive element |\n * |---|---|---|\n * | `<th>` / `[role=\"columnheader\"\\|\"rowheader\"]` | on the host | inner container (`role=\"button\"`) |\n * | `<span>` / `<div>` (no role) | **not emitted** | inner container (`role=\"button\"`) |\n * | `<button>` / `<a href>` / `[role=\"button\"\\|\"link\"]` | **not emitted** | the host itself |\n *\n * `aria-sort` is only valid on a `columnheader` / `rowheader`, so emitting it on a `<span>` or\n * `<button>` is an axe `aria-allowed-attr` failure and is silently ignored by assistive tech. When\n * the header is composed *inside* a header cell — `<th><span tw-sort-header>…</span></th>`, the\n * shape `tw-table` generates — the `<th>` owns `aria-sort` (see `tw-column`'s `sortState` input,\n * which auto-resolves against the same `[twSort]` directive). Outside a table there is no\n * `columnheader` to carry the state; the header remains a labelled button and the sort direction\n * is conveyed by the arrow plus the consumer's own live region.\n */\n// Selector is deliberately kebab-case to read naturally as an HTML attribute on\n// host elements (e.g. `<th tw-sort-header>`). Camelcasing it to `twSortHeader`\n// would break the public API and ~20 demo usages.\n@Component({\n // eslint-disable-next-line @angular-eslint/component-selector\n selector: '[tw-sort-header]',\n exportAs: 'twSortHeader',\n changeDetection: ChangeDetectionStrategy.OnPush,\n imports: [NgTemplateOutlet],\n templateUrl: './sort-header.html',\n host: {\n '[class]': 'hostClasses()',\n '[attr.aria-sort]': 'hostAriaSort()',\n // Sort state as a data attribute, on EVERY host shape.\n //\n // `aria-sort` is only legal on a header cell, so on a span or button host\n // it is deliberately absent — which left the sort state with no stable,\n // valid hook at all on those hosts. The direction was readable only from\n // the arrow's rotation utility class, which is implementation detail no\n // test or consumer should depend on. A `data-*` attribute is valid on any\n // element and carries no ARIA semantics, so it states the same thing\n // without lying to assistive tech. Values mirror `aria-sort` exactly, so\n // a reader does not have to learn a second vocabulary.\n '[attr.data-sort-direction]': 'ariaSort()',\n '[attr.aria-disabled]': 'isDisabled() ? \"true\" : null',\n '(click)': 'handleClick()',\n '(keydown)': 'handleKeydown($event)',\n },\n})\nexport class SortHeaderComponent implements OnInit, AfterViewInit, OnDestroy {\n /** Unique id for this header. Required — identifies the field/column this header sorts. */\n readonly id = input.required<string>();\n\n /** Overrides the parent directive's `start` for this header only. `undefined` inherits from the parent. */\n readonly start = input<'asc' | 'desc' | undefined>(undefined);\n\n /** Overrides the parent directive's `disableClear` for this header only. `undefined` inherits from the parent. */\n readonly disableClear = input<boolean | undefined>(undefined);\n\n /** When true, this header is disabled — clicks and keyboard activation are ignored. Defaults to `false`. */\n readonly headerDisabled = input<boolean>(false, { alias: 'disabled' });\n\n /** Whether the sort arrow renders `'before'` or `'after'` the projected label. Defaults to `'after'`. */\n readonly arrowPosition = input<TwSortArrowPosition>('after');\n\n /** Semantic color used to tint the arrow when this header is active. Defaults to `'primary'`. */\n readonly color = input<TwColor>('primary');\n\n /** Controls padding and font size. Uses the shared `TwSize` scale. Defaults to `'md'`. */\n readonly size = input<TwSize>('md');\n\n /** Accessible description applied via `AriaDescriber` so screen readers announce the sort action alongside the header text. Defaults to `'Sort'`. */\n readonly sortActionDescription = input<string>('Sort');\n\n private readonly parent = inject(SortDirective, { optional: true });\n private readonly focusMonitor = inject(FocusMonitor);\n private readonly ariaDescriber = inject(AriaDescriber, { optional: true });\n\n private readonly hostEl: HTMLElement = inject(ElementRef).nativeElement;\n\n /**\n * Whether the host element can legally carry `aria-sort`. Resolved once, at construction — the\n * host's tag name and static attributes are already applied by then, and a later `[attr.role]`\n * binding would make this timing-dependent.\n */\n private readonly hostIsHeaderCell = isHeaderCell(this.hostEl);\n\n /** Whether the host is already a control, in which case the inner container must stay inert. */\n private readonly hostIsControl = isInteractiveHost(this.hostEl);\n\n private readonly containerRef =\n viewChild<ElementRef<HTMLElement>>('container');\n\n private _describedBy: string | null = null;\n\n constructor() {\n if (!this.parent && isDevMode()) {\n throw new Error(\n 'tw-sort-header must be placed within a parent element with the twSort directive.',\n );\n }\n\n // Reactive: re-tags the host whenever `sortActionDescription` (a public\n // signal input) changes; consumers may swap the description at runtime for\n // i18n / state-driven copy. Cleanup runs in `ngOnDestroy`. Not a one-shot\n // `afterNextRender`: that would freeze the description at first render and\n // silently ignore subsequent rebinds.\n effect(() => {\n const description = this.sortActionDescription();\n const el = this.controlElement();\n if (!el || !this.ariaDescriber) return;\n if (this._describedBy) {\n this.ariaDescriber.removeDescription(el, this._describedBy);\n }\n this.ariaDescriber.describe(el, description);\n this._describedBy = description;\n });\n }\n\n // ── Derived signals ──\n\n /** Whether this header is currently the active (sorted) header. */\n readonly isActive = computed(\n () => this.parent?.active() === this.id(),\n );\n\n /** Whether this header is disabled via its own input or the parent directive's `disabled`. */\n readonly isDisabled = computed(\n () => this.headerDisabled() || (this.parent?.disabled() ?? false),\n );\n\n /** Active direction for *this* header (null when not active). */\n readonly effectiveDirection = computed<SortDirection>(() =>\n this.isActive() ? (this.parent?.direction() ?? null) : null,\n );\n\n /** `aria-sort` value: `'ascending' | 'descending' | 'none'`. */\n readonly ariaSort = computed(() => {\n const d = this.effectiveDirection();\n if (d === 'asc') return 'ascending';\n if (d === 'desc') return 'descending';\n return 'none';\n });\n\n /**\n * @internal Value actually written to the host's `aria-sort` attribute. `null` — i.e. no\n * attribute — unless the host is a `columnheader` / `rowheader`; see the class remarks.\n */\n readonly hostAriaSort = computed(() =>\n this.hostIsHeaderCell ? this.ariaSort() : null,\n );\n\n /** @internal `role` for the inner container: `'button'` unless the host is already the control. */\n readonly controlRole = computed(() =>\n this.hostIsControl || this.isDisabled() ? null : 'button',\n );\n\n /** @internal `tabindex` for the inner container. Mirrors `controlRole()`. */\n readonly controlTabIndex = computed(() =>\n this.hostIsControl || this.isDisabled() ? null : 0,\n );\n\n /** Whether the arrow element should render. Active headers always render; inactive render when not disabled so hover reveals the affordance. */\n readonly renderArrow = computed(() => this.isActive() || !this.isDisabled());\n\n /**\n * The element that carries focus and the accessible description — the host when the consumer\n * mounted the header on a control, the inner container otherwise.\n */\n private controlElement(): HTMLElement | undefined {\n return this.hostIsControl\n ? this.hostEl\n : this.containerRef()?.nativeElement;\n }\n\n // ── Variant class computation ──\n\n private readonly variantResult = computed(() =>\n sortHeaderVariants({\n size: this.size(),\n active: this.isActive(),\n direction: this.effectiveDirection() ?? 'none',\n disabled: this.isDisabled(),\n hostControl: this.hostIsControl,\n }),\n );\n\n /** @internal */\n readonly hostClasses = computed(() => this.variantResult().host());\n\n /** @internal */\n readonly containerClasses = computed(() => this.variantResult().container());\n\n /** @internal */\n readonly labelClasses = computed(() => this.variantResult().label());\n\n /** @internal */\n readonly arrowClasses = computed(() => {\n const base = this.variantResult().arrow();\n if (!this.isActive()) return base;\n return `${base} ${ARROW_ACTIVE_COLOR[this.color()]}`;\n });\n\n /** @internal */\n readonly arrowIconClasses = computed(() => this.variantResult().arrowIcon());\n\n // ── Lifecycle ──\n\n ngOnInit(): void {\n this.parent?.register(this.id());\n }\n\n ngAfterViewInit(): void {\n const el = this.controlElement();\n if (el) {\n this.focusMonitor.monitor(el, true);\n }\n }\n\n ngOnDestroy(): void {\n const el = this.controlElement();\n if (el) {\n this.focusMonitor.stopMonitoring(el);\n if (this._describedBy && this.ariaDescriber) {\n this.ariaDescriber.removeDescription(el, this._describedBy);\n }\n }\n this.parent?.deregister(this.id());\n }\n\n // ── Event handlers ──\n\n /** @internal */\n handleClick(): void {\n if (this.isDisabled()) return;\n this.parent?.sort(snapshot(this));\n }\n\n /** @internal */\n handleKeydown(event: KeyboardEvent): void {\n if (this.isDisabled()) return;\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault();\n this.parent?.sort(snapshot(this));\n }\n }\n}\n","<!--\n The container is the interactive control (role=button + tabindex) unless the\n consumer already mounted the header on one - a button, an anchor, or an\n element carrying a static button/link role. Nesting a second widget inside\n such a host is an axe nested-interactive failure and leaves assistive tech\n with two competing activation targets, so controlRole and controlTabIndex\n both resolve to null there and the host keeps the focus ring instead.\n-->\n<div\n #container\n [class]=\"containerClasses()\"\n [attr.role]=\"controlRole()\"\n [attr.tabindex]=\"controlTabIndex()\"\n>\n @if (arrowPosition() === 'before' && renderArrow()) {\n <ng-container *ngTemplateOutlet=\"arrow\" />\n }\n <span [class]=\"labelClasses()\"><ng-content /></span>\n @if (arrowPosition() === 'after' && renderArrow()) {\n <ng-container *ngTemplateOutlet=\"arrow\" />\n }\n</div>\n\n<ng-template #arrow>\n <span [class]=\"arrowClasses()\" data-tw-sort-arrow>\n <ng-content select=\"[twSortHeaderIcon]\">\n <svg\n viewBox=\"0 0 20 20\"\n fill=\"currentColor\"\n aria-hidden=\"true\"\n [class]=\"arrowIconClasses()\"\n >\n <path\n fill-rule=\"evenodd\"\n d=\"M5.22 8.22a.75.75 0 0 1 1.06 0L10 11.94l3.72-3.72a.75.75 0 1 1 1.06 1.06l-4.25 4.25a.75.75 0 0 1-1.06 0L5.22 9.28a.75.75 0 0 1 0-1.06Z\"\n clip-rule=\"evenodd\"\n />\n </svg>\n </ng-content>\n </span>\n</ng-template>\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
@@ -293,10 +293,22 @@ function hasMinSizeOverflow(panes, unit, availablePx) {
293
293
  }
294
294
 
295
295
  let nextSplitId = 0;
296
+ /**
297
+ * WCAG 2.2 SC 2.5.8 (Target Size Minimum) floor, in CSS px. A gutter thinner
298
+ * than this pads itself out to the floor with transparent padding — see
299
+ * `_gutterHitPad`.
300
+ */
301
+ const TARGET_SIZE_MIN_PX = 24;
296
302
  const splitVariants = tv({
297
303
  slots: {
298
304
  root: 'flex h-full w-full',
299
- gutter: 'shrink-0 bg-border transition-colors duration-normal motion-reduce:transition-none hover:bg-border-strong focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500',
305
+ // `relative` and `bg-clip-content` are load-bearing for the SC 2.5.8 hit
306
+ // area (see `_gutterHitPad`): the element's border box is padded out to
307
+ // 24px so the pointer target meets the floor, `bg-clip-content` keeps the
308
+ // paint inside the `gutterSize`-thick content box, and `relative` puts
309
+ // the gutter in the positioned-descendant paint phase so its overhang
310
+ // wins hit-testing over the adjacent panes' in-flow content.
311
+ gutter: 'relative shrink-0 bg-border bg-clip-content transition-colors duration-normal motion-reduce:transition-none hover:bg-border-strong focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500',
300
312
  },
301
313
  variants: {
302
314
  direction: {
@@ -332,7 +344,7 @@ class SplitComponent {
332
344
  /** How sizes are expressed and reported. A single unit governs the whole container. Defaults to `'percent'`. */
333
345
  unit = input('percent', /* @ts-ignore */
334
346
  ...(ngDevMode ? [{ debugName: "unit" }] : /* istanbul ignore next */ []));
335
- /** Thickness of each gutter in pixels, perpendicular to the split axis. Defaults to `6`. */
347
+ /** Painted thickness of each gutter in pixels, perpendicular to the split axis, and the space it takes from the panes. The pointer target is padded out to a 24px minimum independently of this value (WCAG 2.2 SC 2.5.8), so a thin gutter stays thin without becoming hard to grab. Defaults to `6`. */
336
348
  gutterSize = input(6, /* @ts-ignore */
337
349
  ...(ngDevMode ? [{ debugName: "gutterSize" }] : /* istanbul ignore next */ []));
338
350
  /** When true, all resize interactions are disabled. Gutters are not focusable and do not respond to input. Defaults to `false`. */
@@ -391,8 +403,13 @@ class SplitComponent {
391
403
  */
392
404
  _sizes = signal([], /* @ts-ignore */
393
405
  ...(ngDevMode ? [{ debugName: "_sizes" }] : /* istanbul ignore next */ []));
394
- /** Tracks the live `dir` attribute resolved through CDK Directionality. */
395
- _cdkDir = signal(this._directionality?.value ?? 'ltr', /* @ts-ignore */
406
+ /**
407
+ * Live `dir` resolved through CDK `Directionality`. Read straight from
408
+ * `valueSignal` (CDK 22+) so `_isRtl` re-evaluates on a runtime `dir` flip
409
+ * with no manual subscription mirror. Falls back to 'ltr' when the consumer's
410
+ * injector cannot supply the token (`{ optional: true }`).
411
+ */
412
+ _cdkDir = computed(() => this._directionality?.valueSignal() ?? 'ltr', /* @ts-ignore */
396
413
  ...(ngDevMode ? [{ debugName: "_cdkDir" }] : /* istanbul ignore next */ []));
397
414
  /** Resolves the effective RTL state: explicit `rtl` input overrides CDK `dir`. */
398
415
  _isRtl = computed(() => this.rtl() ?? this._cdkDir() === 'rtl', /* @ts-ignore */
@@ -412,6 +429,35 @@ class SplitComponent {
412
429
  ...(ngDevMode ? [{ debugName: "_rootClass" }] : /* istanbul ignore next */ []));
413
430
  _gutterClass = computed(() => this._classes().gutter(), /* @ts-ignore */
414
431
  ...(ngDevMode ? [{ debugName: "_gutterClass" }] : /* istanbul ignore next */ []));
432
+ /**
433
+ * @internal
434
+ * Transparent padding added to each side of the gutter so its pointer target
435
+ * reaches the WCAG 2.2 SC 2.5.8 24x24 floor while the painted line stays
436
+ * `gutterSize` thick (the default gutter is 6px — a quarter of the floor).
437
+ *
438
+ * The padding is cancelled by an equal negative margin on the same side, so
439
+ * the gutter's *layout* footprint is still exactly `gutterSize` and the
440
+ * `availableSpace()` math is untouched; only its border box — which is what
441
+ * receives the pointer and what a target-size audit measures — grows. The
442
+ * overhang reaches `_gutterHitPad` px into each neighbouring pane and takes
443
+ * pointer events there (including over a pane's own scrollbar, if it has
444
+ * one); that is the accepted cost of the technique and mirrors how VS Code
445
+ * and every other thin-sash implementation reaches the floor.
446
+ *
447
+ * Zero once `gutterSize` is 24 or more — a gutter that already clears the
448
+ * floor needs no padding and no overhang.
449
+ */
450
+ _gutterHitPad = computed(() => Math.max(0, (TARGET_SIZE_MIN_PX - this.gutterSize()) / 2), /* @ts-ignore */
451
+ ...(ngDevMode ? [{ debugName: "_gutterHitPad" }] : /* istanbul ignore next */ []));
452
+ /** @internal Horizontal half of `_gutterHitPad` — non-zero only for a horizontal split, where the gutter is thin across the X axis. Physical (not logical) properties: the padding is symmetric, so RTL needs no separate handling. */
453
+ _gutterHitPadX = computed(() => this.direction() === 'horizontal' ? this._gutterHitPad() : 0, /* @ts-ignore */
454
+ ...(ngDevMode ? [{ debugName: "_gutterHitPadX" }] : /* istanbul ignore next */ []));
455
+ /** @internal Vertical half of `_gutterHitPad` — non-zero only for a vertical split. */
456
+ _gutterHitPadY = computed(() => this.direction() === 'vertical' ? this._gutterHitPad() : 0, /* @ts-ignore */
457
+ ...(ngDevMode ? [{ debugName: "_gutterHitPadY" }] : /* istanbul ignore next */ []));
458
+ /** @internal Gutter border-box thickness: `gutterSize` plus the SC 2.5.8 hit padding on both sides, i.e. `max(gutterSize, 24)`. The negative margins give back the padding, so the *layout* footprint stays `gutterSize`. */
459
+ _gutterBoxPx = computed(() => this.gutterSize() + this._gutterHitPad() * 2, /* @ts-ignore */
460
+ ...(ngDevMode ? [{ debugName: "_gutterBoxPx" }] : /* istanbul ignore next */ []));
415
461
  /** Indices `0..panes.length - 2` representing the gutter between pane[i] and pane[i+1]. */
416
462
  _gutterIndices = computed(() => {
417
463
  const n = this._panes().length;
@@ -446,15 +492,6 @@ class SplitComponent {
446
492
  const containerPx = untracked(() => this._containerSizePx());
447
493
  this._onPanesChange(panes, containerPx);
448
494
  });
449
- // Reflect CDK Directionality changes into the local signal so the computed
450
- // _isRtl re-evaluates and keyboard / pointer handlers honour live `dir`
451
- // attribute changes (e.g. when an ancestor flips the page direction).
452
- if (this._directionality) {
453
- const sub = this._directionality.change.subscribe(value => {
454
- this._cdkDir.set(value);
455
- });
456
- this._destroyRef.onDestroy(() => sub.unsubscribe());
457
- }
458
495
  }
459
496
  // ── Internal resize handler ─────────────────────────────────────────────────
460
497
  /**
@@ -1021,9 +1058,17 @@ class SplitComponent {
1021
1058
  [attr.tabindex]="disabled() ? -1 : 0"
1022
1059
  [class]="_gutterClass()"
1023
1060
  [style.order]="i * 2 + 1"
1024
- [style.flex-basis.px]="gutterSize()"
1025
- [style.min-width.px]="direction() === 'horizontal' ? gutterSize() : 0"
1026
- [style.min-height.px]="direction() === 'vertical' ? gutterSize() : 0"
1061
+ [style.flex-basis.px]="_gutterBoxPx()"
1062
+ [style.min-width.px]="direction() === 'horizontal' ? _gutterBoxPx() : 0"
1063
+ [style.min-height.px]="direction() === 'vertical' ? _gutterBoxPx() : 0"
1064
+ [style.padding-left.px]="_gutterHitPadX()"
1065
+ [style.padding-right.px]="_gutterHitPadX()"
1066
+ [style.padding-top.px]="_gutterHitPadY()"
1067
+ [style.padding-bottom.px]="_gutterHitPadY()"
1068
+ [style.margin-left.px]="-_gutterHitPadX()"
1069
+ [style.margin-right.px]="-_gutterHitPadX()"
1070
+ [style.margin-top.px]="-_gutterHitPadY()"
1071
+ [style.margin-bottom.px]="-_gutterHitPadY()"
1027
1072
  (pointerdown)="_onGutterPointerDown($event, i)"
1028
1073
  (keydown)="_onGutterKeydown($event, i)"
1029
1074
  ></div>
@@ -1054,9 +1099,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImpor
1054
1099
  [attr.tabindex]="disabled() ? -1 : 0"
1055
1100
  [class]="_gutterClass()"
1056
1101
  [style.order]="i * 2 + 1"
1057
- [style.flex-basis.px]="gutterSize()"
1058
- [style.min-width.px]="direction() === 'horizontal' ? gutterSize() : 0"
1059
- [style.min-height.px]="direction() === 'vertical' ? gutterSize() : 0"
1102
+ [style.flex-basis.px]="_gutterBoxPx()"
1103
+ [style.min-width.px]="direction() === 'horizontal' ? _gutterBoxPx() : 0"
1104
+ [style.min-height.px]="direction() === 'vertical' ? _gutterBoxPx() : 0"
1105
+ [style.padding-left.px]="_gutterHitPadX()"
1106
+ [style.padding-right.px]="_gutterHitPadX()"
1107
+ [style.padding-top.px]="_gutterHitPadY()"
1108
+ [style.padding-bottom.px]="_gutterHitPadY()"
1109
+ [style.margin-left.px]="-_gutterHitPadX()"
1110
+ [style.margin-right.px]="-_gutterHitPadX()"
1111
+ [style.margin-top.px]="-_gutterHitPadY()"
1112
+ [style.margin-bottom.px]="-_gutterHitPadY()"
1060
1113
  (pointerdown)="_onGutterPointerDown($event, i)"
1061
1114
  (keydown)="_onGutterKeydown($event, i)"
1062
1115
  ></div>