@cdevhub/ngx-tw 0.2.0 → 0.3.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 (144) hide show
  1. package/README.md +264 -49
  2. package/fesm2022/cdevhub-ngx-tw-accordion.mjs +11 -7
  3. package/fesm2022/cdevhub-ngx-tw-accordion.mjs.map +1 -1
  4. package/fesm2022/cdevhub-ngx-tw-alert.mjs +43 -29
  5. package/fesm2022/cdevhub-ngx-tw-alert.mjs.map +1 -1
  6. package/fesm2022/cdevhub-ngx-tw-aspect-ratio.mjs +103 -0
  7. package/fesm2022/cdevhub-ngx-tw-aspect-ratio.mjs.map +1 -0
  8. package/fesm2022/cdevhub-ngx-tw-avatar.mjs +56 -31
  9. package/fesm2022/cdevhub-ngx-tw-avatar.mjs.map +1 -1
  10. package/fesm2022/cdevhub-ngx-tw-badge.mjs +61 -35
  11. package/fesm2022/cdevhub-ngx-tw-badge.mjs.map +1 -1
  12. package/fesm2022/cdevhub-ngx-tw-breadcrumbs.mjs +154 -93
  13. package/fesm2022/cdevhub-ngx-tw-breadcrumbs.mjs.map +1 -1
  14. package/fesm2022/cdevhub-ngx-tw-button.mjs +35 -22
  15. package/fesm2022/cdevhub-ngx-tw-button.mjs.map +1 -1
  16. package/fesm2022/cdevhub-ngx-tw-calendar-luxon.mjs +3 -3
  17. package/fesm2022/cdevhub-ngx-tw-calendar-luxon.mjs.map +1 -1
  18. package/fesm2022/cdevhub-ngx-tw-calendar.mjs +416 -216
  19. package/fesm2022/cdevhub-ngx-tw-calendar.mjs.map +1 -1
  20. package/fesm2022/cdevhub-ngx-tw-card.mjs +33 -24
  21. package/fesm2022/cdevhub-ngx-tw-card.mjs.map +1 -1
  22. package/fesm2022/cdevhub-ngx-tw-carousel.mjs +152 -81
  23. package/fesm2022/cdevhub-ngx-tw-carousel.mjs.map +1 -1
  24. package/fesm2022/cdevhub-ngx-tw-checkbox.mjs +99 -47
  25. package/fesm2022/cdevhub-ngx-tw-checkbox.mjs.map +1 -1
  26. package/fesm2022/cdevhub-ngx-tw-code-block.mjs +42 -29
  27. package/fesm2022/cdevhub-ngx-tw-code-block.mjs.map +1 -1
  28. package/fesm2022/cdevhub-ngx-tw-collapsible.mjs +46 -29
  29. package/fesm2022/cdevhub-ngx-tw-collapsible.mjs.map +1 -1
  30. package/fesm2022/cdevhub-ngx-tw-combobox.mjs +205 -110
  31. package/fesm2022/cdevhub-ngx-tw-combobox.mjs.map +1 -1
  32. package/fesm2022/cdevhub-ngx-tw-command-palette.mjs +141 -74
  33. package/fesm2022/cdevhub-ngx-tw-command-palette.mjs.map +1 -1
  34. package/fesm2022/cdevhub-ngx-tw-core.mjs +16 -11
  35. package/fesm2022/cdevhub-ngx-tw-core.mjs.map +1 -1
  36. package/fesm2022/cdevhub-ngx-tw-date-picker.mjs +395 -149
  37. package/fesm2022/cdevhub-ngx-tw-date-picker.mjs.map +1 -1
  38. package/fesm2022/cdevhub-ngx-tw-date-range-picker.mjs +305 -150
  39. package/fesm2022/cdevhub-ngx-tw-date-range-picker.mjs.map +1 -1
  40. package/fesm2022/cdevhub-ngx-tw-dialog.mjs +56 -43
  41. package/fesm2022/cdevhub-ngx-tw-dialog.mjs.map +1 -1
  42. package/fesm2022/cdevhub-ngx-tw-empty-state.mjs +51 -33
  43. package/fesm2022/cdevhub-ngx-tw-empty-state.mjs.map +1 -1
  44. package/fesm2022/cdevhub-ngx-tw-file-upload.mjs +905 -0
  45. package/fesm2022/cdevhub-ngx-tw-file-upload.mjs.map +1 -0
  46. package/fesm2022/cdevhub-ngx-tw-flip-card.mjs +49 -26
  47. package/fesm2022/cdevhub-ngx-tw-flip-card.mjs.map +1 -1
  48. package/fesm2022/cdevhub-ngx-tw-form-field.mjs +133 -74
  49. package/fesm2022/cdevhub-ngx-tw-form-field.mjs.map +1 -1
  50. package/fesm2022/cdevhub-ngx-tw-icon-lucide.mjs +2 -2
  51. package/fesm2022/cdevhub-ngx-tw-icon-lucide.mjs.map +1 -1
  52. package/fesm2022/cdevhub-ngx-tw-icon.mjs +28 -17
  53. package/fesm2022/cdevhub-ngx-tw-icon.mjs.map +1 -1
  54. package/fesm2022/cdevhub-ngx-tw-input.mjs +37 -21
  55. package/fesm2022/cdevhub-ngx-tw-input.mjs.map +1 -1
  56. package/fesm2022/cdevhub-ngx-tw-item.mjs +33 -24
  57. package/fesm2022/cdevhub-ngx-tw-item.mjs.map +1 -1
  58. package/fesm2022/cdevhub-ngx-tw-menu.mjs +63 -48
  59. package/fesm2022/cdevhub-ngx-tw-menu.mjs.map +1 -1
  60. package/fesm2022/cdevhub-ngx-tw-number-input.mjs +622 -0
  61. package/fesm2022/cdevhub-ngx-tw-number-input.mjs.map +1 -0
  62. package/fesm2022/cdevhub-ngx-tw-paginator.mjs +142 -72
  63. package/fesm2022/cdevhub-ngx-tw-paginator.mjs.map +1 -1
  64. package/fesm2022/cdevhub-ngx-tw-popover.mjs +101 -46
  65. package/fesm2022/cdevhub-ngx-tw-popover.mjs.map +1 -1
  66. package/fesm2022/cdevhub-ngx-tw-progress-bar.mjs +57 -30
  67. package/fesm2022/cdevhub-ngx-tw-progress-bar.mjs.map +1 -1
  68. package/fesm2022/cdevhub-ngx-tw-radio.mjs +108 -57
  69. package/fesm2022/cdevhub-ngx-tw-radio.mjs.map +1 -1
  70. package/fesm2022/cdevhub-ngx-tw-segmented-control.mjs +46 -26
  71. package/fesm2022/cdevhub-ngx-tw-segmented-control.mjs.map +1 -1
  72. package/fesm2022/cdevhub-ngx-tw-select.mjs +227 -122
  73. package/fesm2022/cdevhub-ngx-tw-select.mjs.map +1 -1
  74. package/fesm2022/cdevhub-ngx-tw-separator.mjs +21 -12
  75. package/fesm2022/cdevhub-ngx-tw-separator.mjs.map +1 -1
  76. package/fesm2022/cdevhub-ngx-tw-sheet.mjs +58 -44
  77. package/fesm2022/cdevhub-ngx-tw-sheet.mjs.map +1 -1
  78. package/fesm2022/cdevhub-ngx-tw-skeleton.mjs +25 -14
  79. package/fesm2022/cdevhub-ngx-tw-skeleton.mjs.map +1 -1
  80. package/fesm2022/cdevhub-ngx-tw-slider.mjs +155 -79
  81. package/fesm2022/cdevhub-ngx-tw-slider.mjs.map +1 -1
  82. package/fesm2022/cdevhub-ngx-tw-sort.mjs +47 -25
  83. package/fesm2022/cdevhub-ngx-tw-sort.mjs.map +1 -1
  84. package/fesm2022/cdevhub-ngx-tw-spinner.mjs +29 -16
  85. package/fesm2022/cdevhub-ngx-tw-spinner.mjs.map +1 -1
  86. package/fesm2022/cdevhub-ngx-tw-split.mjs +72 -42
  87. package/fesm2022/cdevhub-ngx-tw-split.mjs.map +1 -1
  88. package/fesm2022/cdevhub-ngx-tw-stat.mjs +107 -64
  89. package/fesm2022/cdevhub-ngx-tw-stat.mjs.map +1 -1
  90. package/fesm2022/cdevhub-ngx-tw-stepper.mjs +113 -47
  91. package/fesm2022/cdevhub-ngx-tw-stepper.mjs.map +1 -1
  92. package/fesm2022/cdevhub-ngx-tw-switch.mjs +61 -32
  93. package/fesm2022/cdevhub-ngx-tw-switch.mjs.map +1 -1
  94. package/fesm2022/cdevhub-ngx-tw-tab-nav.mjs +88 -42
  95. package/fesm2022/cdevhub-ngx-tw-tab-nav.mjs.map +1 -1
  96. package/fesm2022/cdevhub-ngx-tw-table.mjs +266 -118
  97. package/fesm2022/cdevhub-ngx-tw-table.mjs.map +1 -1
  98. package/fesm2022/cdevhub-ngx-tw-tabs.mjs +102 -52
  99. package/fesm2022/cdevhub-ngx-tw-tabs.mjs.map +1 -1
  100. package/fesm2022/cdevhub-ngx-tw-tags-input.mjs +798 -0
  101. package/fesm2022/cdevhub-ngx-tw-tags-input.mjs.map +1 -0
  102. package/fesm2022/cdevhub-ngx-tw-textarea.mjs +16 -8
  103. package/fesm2022/cdevhub-ngx-tw-textarea.mjs.map +1 -1
  104. package/fesm2022/cdevhub-ngx-tw-theme.mjs +22 -14
  105. package/fesm2022/cdevhub-ngx-tw-theme.mjs.map +1 -1
  106. package/fesm2022/cdevhub-ngx-tw-time-picker.mjs +243 -95
  107. package/fesm2022/cdevhub-ngx-tw-time-picker.mjs.map +1 -1
  108. package/fesm2022/cdevhub-ngx-tw-timeline.mjs +149 -82
  109. package/fesm2022/cdevhub-ngx-tw-timeline.mjs.map +1 -1
  110. package/fesm2022/cdevhub-ngx-tw-toast.mjs +100 -55
  111. package/fesm2022/cdevhub-ngx-tw-toast.mjs.map +1 -1
  112. package/fesm2022/cdevhub-ngx-tw-tooltip.mjs +50 -28
  113. package/fesm2022/cdevhub-ngx-tw-tooltip.mjs.map +1 -1
  114. package/fesm2022/cdevhub-ngx-tw-transfer.mjs +1129 -0
  115. package/fesm2022/cdevhub-ngx-tw-transfer.mjs.map +1 -0
  116. package/fesm2022/cdevhub-ngx-tw-tree.mjs +554 -0
  117. package/fesm2022/cdevhub-ngx-tw-tree.mjs.map +1 -0
  118. package/fesm2022/cdevhub-ngx-tw.mjs +7 -0
  119. package/fesm2022/cdevhub-ngx-tw.mjs.map +1 -1
  120. package/package.json +45 -17
  121. package/theme/_dark.css +16 -12
  122. package/theme/_semantic.css +3 -2
  123. package/theme/index.css +10 -2
  124. package/types/cdevhub-ngx-tw-aspect-ratio.d.ts +40 -0
  125. package/types/cdevhub-ngx-tw-calendar.d.ts +39 -8
  126. package/types/cdevhub-ngx-tw-code-block.d.ts +8 -7
  127. package/types/cdevhub-ngx-tw-command-palette.d.ts +7 -0
  128. package/types/cdevhub-ngx-tw-date-picker.d.ts +36 -10
  129. package/types/cdevhub-ngx-tw-file-upload.d.ts +250 -0
  130. package/types/cdevhub-ngx-tw-form-field.d.ts +22 -3
  131. package/types/cdevhub-ngx-tw-icon-lucide.d.ts +21 -3
  132. package/types/cdevhub-ngx-tw-number-input.d.ts +130 -0
  133. package/types/cdevhub-ngx-tw-popover.d.ts +8 -0
  134. package/types/cdevhub-ngx-tw-select.d.ts +1 -1
  135. package/types/cdevhub-ngx-tw-stepper.d.ts +30 -2
  136. package/types/cdevhub-ngx-tw-tab-nav.d.ts +8 -0
  137. package/types/cdevhub-ngx-tw-table.d.ts +30 -1
  138. package/types/cdevhub-ngx-tw-tabs.d.ts +8 -0
  139. package/types/cdevhub-ngx-tw-tags-input.d.ts +217 -0
  140. package/types/cdevhub-ngx-tw-time-picker.d.ts +31 -6
  141. package/types/cdevhub-ngx-tw-toast.d.ts +1 -1
  142. package/types/cdevhub-ngx-tw-transfer.d.ts +330 -0
  143. package/types/cdevhub-ngx-tw-tree.d.ts +189 -0
  144. package/types/cdevhub-ngx-tw.d.ts +7 -0
@@ -0,0 +1,1129 @@
1
+ import { NgTemplateOutlet } from '@angular/common';
2
+ import * as i0 from '@angular/core';
3
+ import { inject, TemplateRef, Directive, input, output, contentChild, ElementRef, Injector, DestroyRef, signal, computed, linkedSignal, afterNextRender, isDevMode, forwardRef, ChangeDetectionStrategy, Component } from '@angular/core';
4
+ import { NgControl, NgForm, FormGroupDirective, Validators } from '@angular/forms';
5
+ import { CdkListbox, CdkOption } from '@angular/cdk/listbox';
6
+ import { FocusMonitor, LiveAnnouncer } from '@angular/cdk/a11y';
7
+ import { Platform } from '@angular/cdk/platform';
8
+ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
9
+ import { merge } from 'rxjs';
10
+ import { tv } from 'tailwind-variants';
11
+ import { TW_ERROR_STATE_MATCHER } from '@cdevhub/ngx-tw/core';
12
+ import { FormFieldControl, TW_FORM_FIELD_CONTROL } from '@cdevhub/ngx-tw/form-field';
13
+ import { CheckboxComponent } from '@cdevhub/ngx-tw/checkbox';
14
+ import { InputDirective } from '@cdevhub/ngx-tw/input';
15
+
16
+ /*
17
+ * tw-transfer — dual-listbox shuttle. Two panels (source / target) with a column
18
+ * of move buttons between them; the user ticks items in a panel and shuttles the
19
+ * ticked items to the other side. The component's *value* is the set of keys
20
+ * currently on the target side (`targetKeys`), exposed through
21
+ * `ControlValueAccessor` + `FormFieldControl` (mirrors `tags-input` / `select`).
22
+ *
23
+ * Architecture decisions (verified against @angular/cdk 21.2.x source):
24
+ * - Each panel body is one `[cdkListbox]` (multiple) with one `[cdkOption]` per
25
+ * VISIBLE item. CDK owns `role="listbox"`/`"option"`, `aria-multiselectable`/
26
+ * `aria-selected`, roving-tabindex Arrow/Home/End, Space/Enter toggle, and
27
+ * typeahead. We do NOT roll a custom listbox or reuse select's overlay.
28
+ * - Option value is the item's KEY `K` (`[cdkOption]="row.key"`), so each panel's
29
+ * `cdkListboxValue` is a `readonly K[]` and `compareWith` is key identity.
30
+ * - TWO selection layers, kept distinct:
31
+ * • `targetKeys` (signal) — persisted membership = the CVA value.
32
+ * • per-panel ephemeral "checked" — the CdkListbox value, cleared after moves.
33
+ * - The checked binding is CONTROLLED: `[cdkListboxValue]="<panel>Checked()"`,
34
+ * writing the user's `cdkListboxValueChange` back into that signal. Programmatic
35
+ * writes to `cdkListboxValue` are SILENT in CDK (only user gestures call
36
+ * `valueChange.next` — verified: triggerOption/triggerRange), so the loop never
37
+ * re-enters → no cycle / freeze.
38
+ * - The checked set is a `linkedSignal` whose source is the rendered-key set: on
39
+ * every render change it PRUNES keys whose option is no longer present (a filter
40
+ * hid it, or a move removed it). Two consequences: (1) the bound value is always
41
+ * a subset of the rendered options, so CdkListbox's internal `_invalid` flag —
42
+ * which would otherwise collapse the whole selection to `[]` — never trips; and
43
+ * (2) a key checked then filtered out is dropped permanently (it does NOT
44
+ * re-appear when the filter clears). This matches the requirements doc and side-
45
+ * steps a CDK quirk where a controlled value re-surfacing a just-re-rendered
46
+ * option is not reliably re-selected. Moves always act on the visible subset.
47
+ */
48
+ // ── Resolved-default constants ────────────────────────────────────────
49
+ const LABELS_DEFAULTS = {
50
+ sourceTitle: 'Source',
51
+ targetTitle: 'Target',
52
+ searchPlaceholder: 'Search',
53
+ emptyText: 'No items',
54
+ selectAllLabel: 'Select all',
55
+ moveToTargetLabel: 'Move selected to target',
56
+ moveToSourceLabel: 'Move selected to source',
57
+ countFormat: '{total} items',
58
+ moveAnnouncement: '{count} items moved to {target}',
59
+ };
60
+ const DISPLAY_DEFAULTS = {
61
+ size: 'md',
62
+ listHeight: 240,
63
+ showSearch: false,
64
+ showSelectAll: true,
65
+ };
66
+ /** Replaces `{name}` tokens in a template with values from `vars`. */
67
+ function formatLabel(template, vars) {
68
+ return template.replace(/\{(\w+)\}/g, (match, key) => key in vars ? String(vars[key]) : match);
69
+ }
70
+ // ── tv() config ───────────────────────────────────────────────────────
71
+ //
72
+ // All class strings are literal so the Tailwind v4 JIT scanner picks them up.
73
+ // Semantic + surface/fg/border tokens only. The selected/checked option styling
74
+ // mirrors `tree`'s selected node (`bg-primary-50 text-primary-700`, no `dark:`
75
+ // overrides — slot tokens own structural light/dark contrast).
76
+ const transferVariants = tv({
77
+ slots: {
78
+ // panel · controls · panel. The middle column is auto width; both panels
79
+ // share the remaining space and clamp to `min-w-0` so rows can truncate.
80
+ root: 'grid grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)] gap-3 items-stretch text-fg',
81
+ panel: 'flex flex-col min-w-0 rounded-lg border border-border bg-surface overflow-hidden',
82
+ panelHeader: 'flex items-center gap-2 border-b border-border bg-surface-muted',
83
+ panelTitle: 'text-sm font-semibold text-fg min-w-0 truncate flex-1',
84
+ panelCount: 'text-xs text-fg-muted shrink-0 tabular-nums',
85
+ search: 'border-b border-border',
86
+ // The cdkListbox scroll viewport. `outline-none` because focus rides the
87
+ // individual options (canonical ring there), not the container.
88
+ list: 'flex flex-col overflow-y-auto outline-none',
89
+ // The cdkOption row.
90
+ option: 'flex items-center gap-2 cursor-pointer select-none text-fg transition-colors duration-200 motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500',
91
+ // Presentational check glyph (aria-hidden). Always laid out (for alignment);
92
+ // visibility toggles via opacity off the checked state.
93
+ optionCheck: 'shrink-0 transition-opacity duration-200 motion-reduce:transition-none',
94
+ optionLabel: 'min-w-0 truncate flex-1',
95
+ controls: 'flex flex-col justify-center gap-2',
96
+ // Plain square move buttons (icon-only). Square-interactive sub-scale per
97
+ // size; not the [twButton] directive (its size padding would fight a square
98
+ // override across two class bindings).
99
+ moveButton: 'inline-flex items-center justify-center shrink-0 rounded-md border border-border bg-surface text-fg-muted cursor-pointer transition-colors duration-200 motion-reduce:transition-none hover:bg-surface-muted hover:text-fg hover:border-border-strong focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500 disabled:opacity-50 disabled:pointer-events-none disabled:cursor-not-allowed',
100
+ moveIcon: 'shrink-0',
101
+ empty: 'flex items-center justify-center p-4 text-sm text-fg-subtle text-center',
102
+ },
103
+ variants: {
104
+ size: {
105
+ xs: {
106
+ panelHeader: 'px-2 py-1',
107
+ search: 'px-2 py-1',
108
+ option: 'px-2 py-1 text-xs gap-1.5',
109
+ optionCheck: 'size-4',
110
+ moveButton: 'size-6',
111
+ moveIcon: 'size-4',
112
+ empty: 'text-xs',
113
+ },
114
+ sm: {
115
+ panelHeader: 'px-3 py-1.5',
116
+ search: 'px-3 py-1.5',
117
+ option: 'px-3 py-1.5 text-sm',
118
+ optionCheck: 'size-4',
119
+ moveButton: 'size-7',
120
+ moveIcon: 'size-4',
121
+ },
122
+ md: {
123
+ panelHeader: 'px-4 py-2',
124
+ search: 'px-3 py-2',
125
+ option: 'px-3 py-2 text-sm',
126
+ optionCheck: 'size-5',
127
+ moveButton: 'size-8',
128
+ moveIcon: 'size-5',
129
+ },
130
+ lg: {
131
+ panelHeader: 'px-5 py-2.5',
132
+ search: 'px-4 py-2.5',
133
+ option: 'px-4 py-2.5 text-base',
134
+ optionCheck: 'size-5',
135
+ moveButton: 'size-9',
136
+ moveIcon: 'size-5',
137
+ },
138
+ xl: {
139
+ panelHeader: 'px-6 py-3',
140
+ search: 'px-4 py-3',
141
+ option: 'px-4 py-3 text-base',
142
+ optionCheck: 'size-5',
143
+ moveButton: 'size-9',
144
+ moveIcon: 'size-5',
145
+ },
146
+ },
147
+ checked: {
148
+ true: {
149
+ option: 'bg-primary-50 text-primary-700 hover:bg-primary-50',
150
+ optionCheck: 'opacity-100 text-primary-600',
151
+ },
152
+ false: {
153
+ option: 'hover:bg-surface-muted',
154
+ optionCheck: 'opacity-0',
155
+ },
156
+ },
157
+ optionDisabled: {
158
+ true: { option: 'opacity-50 pointer-events-none' },
159
+ false: {},
160
+ },
161
+ disabled: {
162
+ true: { root: 'opacity-50 pointer-events-none' },
163
+ false: {},
164
+ },
165
+ },
166
+ defaultVariants: {
167
+ size: 'md',
168
+ checked: false,
169
+ optionDisabled: false,
170
+ disabled: false,
171
+ },
172
+ }, { twMerge: true });
173
+ // ── TransferItemDefDirective ──────────────────────────────────────────
174
+ //
175
+ // Pure TemplateRef carrier for the consumer's per-item template. Mirrors tree's
176
+ // `TreeNodeDefDirective` / table's `CellDefDirective`: a typed context +
177
+ // `ngTemplateContextGuard` so `let-item` / `let-checked="checked"` type-check.
178
+ // Optional — when absent each row renders the default check glyph + label.
179
+ /** Structural directive (`*twTransferItem="let item"`) declaring the per-item template. Typed as `TwTransferItemContext<T>`. */
180
+ class TransferItemDefDirective {
181
+ /** @internal Consumer-projected per-item template. */
182
+ template = inject((TemplateRef));
183
+ /** @internal */
184
+ static ngTemplateContextGuard(_dir, _ctx) {
185
+ return true;
186
+ }
187
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: TransferItemDefDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
188
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.7", type: TransferItemDefDirective, isStandalone: true, selector: "[twTransferItem]", ngImport: i0 });
189
+ }
190
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: TransferItemDefDirective, decorators: [{
191
+ type: Directive,
192
+ args: [{ selector: '[twTransferItem]' }]
193
+ }] });
194
+ let nextId = 0;
195
+ // ── TransferComponent ─────────────────────────────────────────────────
196
+ /**
197
+ * A dual-listbox shuttle: two panels (source / target) with a column of move
198
+ * buttons between them. The user ticks items in a panel and shuttles them across;
199
+ * an optional per-panel search and a tri-state header select-all accelerate bulk
200
+ * moves. The component's value is the set of keys on the target side, exposed
201
+ * through `ControlValueAccessor` + `FormFieldControl` so it works with reactive,
202
+ * template-driven, and signal forms and integrates with `<tw-form-field>`.
203
+ *
204
+ * Each panel composes `@angular/cdk/listbox` (`CdkListbox` / `CdkOption`) for
205
+ * focus-managed, accessible, keyboard-navigable selection. Consumers supply
206
+ * `keyFn` (required) and `labelFn` (defaulted to `String(item)`), and may project
207
+ * a `*twTransferItem` template for rich row content.
208
+ */
209
+ class TransferComponent extends FormFieldControl {
210
+ // ── Inputs ──
211
+ /** All items across both panels. The source/target split derives from the value, not the reverse. Defaults to `[]`. */
212
+ data = input([], /* @ts-ignore */
213
+ ...(ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []));
214
+ /** Resolves a stable key for an item — drives membership (the value), checked state, tracking, the cdkOption value, and compareWith. Required. */
215
+ keyFn = input.required(/* @ts-ignore */
216
+ ...(ngDevMode ? [{ debugName: "keyFn" }] : /* istanbul ignore next */ []));
217
+ /** Resolves an item's display label — used for the default row render, search filtering, and listbox typeahead. Defaults to `(item) => String(item)`. */
218
+ labelFn = input((item) => String(item), /* @ts-ignore */
219
+ ...(ngDevMode ? [{ debugName: "labelFn" }] : /* istanbul ignore next */ []));
220
+ /** Per-panel text + ARIA labels. Partial; unset keys fall back to the English defaults. */
221
+ labels = input({}, /* @ts-ignore */
222
+ ...(ngDevMode ? [{ debugName: "labels" }] : /* istanbul ignore next */ []));
223
+ /** Visual configuration — size, list viewport height, search / select-all visibility. Partial; unset keys fall back to the defaults. */
224
+ display = input({}, /* @ts-ignore */
225
+ ...(ngDevMode ? [{ debugName: "display" }] : /* istanbul ignore next */ []));
226
+ /** Behavioural configuration — oneWay, custom filterFn, per-item disabledItem predicate. Partial; unset keys fall back to the defaults. */
227
+ behavior = input({}, /* @ts-ignore */
228
+ ...(ngDevMode ? [{ debugName: "behavior" }] : /* istanbul ignore next */ []));
229
+ /** Disables the entire control — panels non-interactive, search + buttons disabled, muted styling. Also driven by Forms `setDisabledState`. Defaults to `false`. */
230
+ disabledInput = input(false, { ...(ngDevMode ? { debugName: "disabledInput" } : /* istanbul ignore next */ {}), alias: 'disabled' });
231
+ /** Marks the control as required. Mirrored to `aria-required` chrome via the form-field; also inferred from `Validators.required` on a bound control. Defaults to `false`. */
232
+ requiredInput = input(false, { ...(ngDevMode ? { debugName: "requiredInput" } : /* istanbul ignore next */ {}), alias: 'required' });
233
+ /** Reserved for control identification (e.g. analytics, future native-submission parity). The transfer has no single native value element, so it is not currently reflected to the DOM. Defaults to `undefined`. */
234
+ name = input(undefined, /* @ts-ignore */
235
+ ...(ngDevMode ? [{ debugName: "name" }] : /* istanbul ignore next */ []));
236
+ /** Id on the host element. Auto-generated as `tw-transfer-N` when unset. Used by the form-field's `<label for>` association. */
237
+ idInput = input(undefined, { ...(ngDevMode ? { debugName: "idInput" } : /* istanbul ignore next */ {}), alias: 'id' });
238
+ /** Accessible name applied to the control when no visible label is wired. Mirrored to `aria-label`. Defaults to `undefined`. */
239
+ ariaLabel = input(undefined, { ...(ngDevMode ? { debugName: "ariaLabel" } : /* istanbul ignore next */ {}), alias: 'aria-label' });
240
+ /** ID of an external element that labels the control. Mirrored to `aria-labelledby`. Defaults to `undefined`. */
241
+ ariaLabelledby = input(undefined, { ...(ngDevMode ? { debugName: "ariaLabelledby" } : /* istanbul ignore next */ {}), alias: 'aria-labelledby' });
242
+ /** ID of an external element that describes the control. The form-field merges its hint / error ids alongside. Defaults to `undefined`. */
243
+ ariaDescribedby = input(undefined, { ...(ngDevMode ? { debugName: "ariaDescribedby" } : /* istanbul ignore next */ {}), alias: 'aria-describedby' });
244
+ /** Per-instance override of the {@link ErrorStateMatcher}. When omitted, uses the `TW_ERROR_STATE_MATCHER` token's value. Defaults to `undefined`. */
245
+ errorStateMatcher = input(undefined, /* @ts-ignore */
246
+ ...(ngDevMode ? [{ debugName: "errorStateMatcher" }] : /* istanbul ignore next */ []));
247
+ // ── Outputs ──
248
+ /** Fires when items move between panels by user interaction. Payload is the new full target-keys array. Does not fire on `writeValue`. */
249
+ valueChange = output();
250
+ /** Fires after each directional move, identifying the moved keys and their direction. Does not fire on `writeValue`. */
251
+ moved = output();
252
+ // ── Content query ──
253
+ /** @internal Consumer-projected per-item template (`*twTransferItem`). */
254
+ itemTemplate = contentChild((TransferItemDefDirective), /* @ts-ignore */
255
+ ...(ngDevMode ? [{ debugName: "itemTemplate" }] : /* istanbul ignore next */ []));
256
+ // ── Injected deps ──
257
+ elementRef = inject((ElementRef));
258
+ injector = inject(Injector);
259
+ destroyRef = inject(DestroyRef);
260
+ focusMonitor = inject(FocusMonitor);
261
+ liveAnnouncer = inject(LiveAnnouncer);
262
+ ngControl = inject(NgControl, { optional: true, self: true });
263
+ parentForm = inject(NgForm, { optional: true });
264
+ parentFormGroup = inject(FormGroupDirective, { optional: true });
265
+ defaultMatcher = inject(TW_ERROR_STATE_MATCHER);
266
+ platform = inject(Platform);
267
+ // ── Identity ──
268
+ uid = nextId++;
269
+ /** @internal Fallback id used when the consumer does not set `[id]`. */
270
+ hostId = `tw-transfer-${this.uid}`;
271
+ sourceTitleId = `${this.hostId}-source-title`;
272
+ targetTitleId = `${this.hostId}-target-title`;
273
+ // ── State signals ──
274
+ /** @internal The value: keys currently on the target side. Source of truth for membership. */
275
+ targetKeys = signal([], /* @ts-ignore */
276
+ ...(ngDevMode ? [{ debugName: "targetKeys" }] : /* istanbul ignore next */ []));
277
+ /** @internal Per-panel search queries. */
278
+ sourceQuery = signal('', /* @ts-ignore */
279
+ ...(ngDevMode ? [{ debugName: "sourceQuery" }] : /* istanbul ignore next */ []));
280
+ targetQuery = signal('', /* @ts-ignore */
281
+ ...(ngDevMode ? [{ debugName: "targetQuery" }] : /* istanbul ignore next */ []));
282
+ cvaDisabled = signal(false, /* @ts-ignore */
283
+ ...(ngDevMode ? [{ debugName: "cvaDisabled" }] : /* istanbul ignore next */ []));
284
+ _focused = signal(false, /* @ts-ignore */
285
+ ...(ngDevMode ? [{ debugName: "_focused" }] : /* istanbul ignore next */ []));
286
+ _ngControlRev = signal(0, /* @ts-ignore */
287
+ ...(ngDevMode ? [{ debugName: "_ngControlRev" }] : /* istanbul ignore next */ []));
288
+ _formSubmitRev = signal(0, /* @ts-ignore */
289
+ ...(ngDevMode ? [{ debugName: "_formSubmitRev" }] : /* istanbul ignore next */ []));
290
+ describedByIdsSignal = signal([], /* @ts-ignore */
291
+ ...(ngDevMode ? [{ debugName: "describedByIdsSignal" }] : /* istanbul ignore next */ []));
292
+ labelledByIdsSignal = signal([], /* @ts-ignore */
293
+ ...(ngDevMode ? [{ debugName: "labelledByIdsSignal" }] : /* istanbul ignore next */ []));
294
+ onChange = () => { };
295
+ onTouched = () => { };
296
+ /** @internal Stable comparator for `[cdkListboxCompareWith]`. Keys are identity-comparable. */
297
+ compareKeys = (a, b) => Object.is(a, b);
298
+ // ── Resolved config ──
299
+ /** @internal */
300
+ resolvedLabels = computed(() => ({
301
+ ...LABELS_DEFAULTS,
302
+ ...this.labels(),
303
+ }), /* @ts-ignore */
304
+ ...(ngDevMode ? [{ debugName: "resolvedLabels" }] : /* istanbul ignore next */ []));
305
+ resolvedDisplay = computed(() => ({
306
+ ...DISPLAY_DEFAULTS,
307
+ ...this.display(),
308
+ }), /* @ts-ignore */
309
+ ...(ngDevMode ? [{ debugName: "resolvedDisplay" }] : /* istanbul ignore next */ []));
310
+ // Behaviour is resolved inline (not via a `Required<>` const) because `filterFn`
311
+ // is legitimately optional and both predicates are generic over `T`.
312
+ resolvedBehavior = computed(() => {
313
+ const b = this.behavior();
314
+ return {
315
+ oneWay: b.oneWay ?? false,
316
+ filterFn: b.filterFn,
317
+ disabledItem: b.disabledItem ?? ((_item) => false),
318
+ };
319
+ }, /* @ts-ignore */
320
+ ...(ngDevMode ? [{ debugName: "resolvedBehavior" }] : /* istanbul ignore next */ []));
321
+ /** @internal */
322
+ resolvedSize = computed(() => this.resolvedDisplay().size, /* @ts-ignore */
323
+ ...(ngDevMode ? [{ debugName: "resolvedSize" }] : /* istanbul ignore next */ []));
324
+ /** @internal Checkbox tops out at `lg` density; map xl → lg. */
325
+ checkboxSize = computed(() => {
326
+ const s = this.resolvedSize();
327
+ return s === 'xl' ? 'lg' : s;
328
+ }, /* @ts-ignore */
329
+ ...(ngDevMode ? [{ debugName: "checkboxSize" }] : /* istanbul ignore next */ []));
330
+ /** @internal */
331
+ showSearch = computed(() => this.resolvedDisplay().showSearch, /* @ts-ignore */
332
+ ...(ngDevMode ? [{ debugName: "showSearch" }] : /* istanbul ignore next */ []));
333
+ /** @internal */
334
+ showSelectAll = computed(() => this.resolvedDisplay().showSelectAll, /* @ts-ignore */
335
+ ...(ngDevMode ? [{ debugName: "showSelectAll" }] : /* istanbul ignore next */ []));
336
+ /** @internal */
337
+ oneWay = computed(() => this.resolvedBehavior().oneWay, /* @ts-ignore */
338
+ ...(ngDevMode ? [{ debugName: "oneWay" }] : /* istanbul ignore next */ []));
339
+ /** @internal Fixed list height in px, or `null` when `'auto'`. */
340
+ listHeightPx = computed(() => {
341
+ const h = this.resolvedDisplay().listHeight;
342
+ return h === 'auto' ? null : h;
343
+ }, /* @ts-ignore */
344
+ ...(ngDevMode ? [{ debugName: "listHeightPx" }] : /* istanbul ignore next */ []));
345
+ // ── Derived membership state ──
346
+ /** @internal Map from key to item across all data — for orphan-aware target rendering and disabled lookups. */
347
+ itemByKey = computed(() => {
348
+ const kf = this.keyFn();
349
+ const map = new Map();
350
+ for (const item of this.data())
351
+ map.set(kf(item), item);
352
+ return map;
353
+ }, /* @ts-ignore */
354
+ ...(ngDevMode ? [{ debugName: "itemByKey" }] : /* istanbul ignore next */ []));
355
+ targetKeySet = computed(() => new Set(this.targetKeys()), /* @ts-ignore */
356
+ ...(ngDevMode ? [{ debugName: "targetKeySet" }] : /* istanbul ignore next */ []));
357
+ /** @internal Items on the source side (in `data` order). */
358
+ sourceItems = computed(() => {
359
+ const inTarget = this.targetKeySet();
360
+ const kf = this.keyFn();
361
+ return this.data().filter((item) => !inTarget.has(kf(item)));
362
+ }, /* @ts-ignore */
363
+ ...(ngDevMode ? [{ debugName: "sourceItems" }] : /* istanbul ignore next */ []));
364
+ /** @internal Items on the target side (in `targetKeys` order; orphan keys with no item are skipped). */
365
+ targetItems = computed(() => {
366
+ const map = this.itemByKey();
367
+ const out = [];
368
+ for (const key of this.targetKeys()) {
369
+ const item = map.get(key);
370
+ if (item !== undefined)
371
+ out.push(item);
372
+ }
373
+ return out;
374
+ }, /* @ts-ignore */
375
+ ...(ngDevMode ? [{ debugName: "targetItems" }] : /* istanbul ignore next */ []));
376
+ // ── Per-panel row pipelines (source) ──
377
+ // Stage 1 (base) carries no checked flag → renderedKeys / reconciled derive from
378
+ // it without a cycle. Stage 2 augments with checked state + precomputed classes.
379
+ sourceRowsBase = computed(() => this.toRowsBase(this.filterItems(this.sourceItems(), this.sourceQuery())), /* @ts-ignore */
380
+ ...(ngDevMode ? [{ debugName: "sourceRowsBase" }] : /* istanbul ignore next */ []));
381
+ sourceRenderedKeys = computed(() => new Set(this.sourceRowsBase().map((r) => r.key)), /* @ts-ignore */
382
+ ...(ngDevMode ? [{ debugName: "sourceRenderedKeys" }] : /* istanbul ignore next */ []));
383
+ /**
384
+ * @internal Ephemeral pending-move checked keys for the source panel — the
385
+ * controlled `[cdkListboxValue]`. A `linkedSignal` over the rendered-key set so
386
+ * it auto-prunes keys whose option a filter / move removed (kept ⊆ rendered → no
387
+ * CdkListbox `_invalid`; dropped keys do not re-surface). Cleared after moves.
388
+ */
389
+ sourceChecked = linkedSignal({ ...(ngDevMode ? { debugName: "sourceChecked" } : /* istanbul ignore next */ {}), source: this.sourceRenderedKeys,
390
+ computation: (rendered, prev) => (prev?.value ?? []).filter((k) => rendered.has(k)) });
391
+ sourceCheckedSet = computed(() => new Set(this.sourceChecked()), /* @ts-ignore */
392
+ ...(ngDevMode ? [{ debugName: "sourceCheckedSet" }] : /* istanbul ignore next */ []));
393
+ sourceRows = computed(() => this.toRows(this.sourceRowsBase(), this.sourceCheckedSet(), 'source'), /* @ts-ignore */
394
+ ...(ngDevMode ? [{ debugName: "sourceRows" }] : /* istanbul ignore next */ []));
395
+ // ── Per-panel row pipelines (target) ──
396
+ targetRowsBase = computed(() => this.toRowsBase(this.filterItems(this.targetItems(), this.targetQuery())), /* @ts-ignore */
397
+ ...(ngDevMode ? [{ debugName: "targetRowsBase" }] : /* istanbul ignore next */ []));
398
+ targetRenderedKeys = computed(() => new Set(this.targetRowsBase().map((r) => r.key)), /* @ts-ignore */
399
+ ...(ngDevMode ? [{ debugName: "targetRenderedKeys" }] : /* istanbul ignore next */ []));
400
+ /** @internal Ephemeral pending-move checked keys for the target panel — see {@link sourceChecked}. */
401
+ targetChecked = linkedSignal({ ...(ngDevMode ? { debugName: "targetChecked" } : /* istanbul ignore next */ {}), source: this.targetRenderedKeys,
402
+ computation: (rendered, prev) => (prev?.value ?? []).filter((k) => rendered.has(k)) });
403
+ targetCheckedSet = computed(() => new Set(this.targetChecked()), /* @ts-ignore */
404
+ ...(ngDevMode ? [{ debugName: "targetCheckedSet" }] : /* istanbul ignore next */ []));
405
+ targetRows = computed(() => this.toRows(this.targetRowsBase(), this.targetCheckedSet(), 'target'), /* @ts-ignore */
406
+ ...(ngDevMode ? [{ debugName: "targetRows" }] : /* istanbul ignore next */ []));
407
+ // ── Movable + select-all derivations ──
408
+ /** @internal Checked, enabled, visible source keys — what the → button moves. */
409
+ sourceMovableKeys = computed(() => {
410
+ const checked = this.sourceCheckedSet();
411
+ return this.sourceRowsBase()
412
+ .filter((r) => !r.disabled && checked.has(r.key))
413
+ .map((r) => r.key);
414
+ }, /* @ts-ignore */
415
+ ...(ngDevMode ? [{ debugName: "sourceMovableKeys" }] : /* istanbul ignore next */ []));
416
+ /** @internal Checked, enabled, visible target keys — what the ← button moves. */
417
+ targetMovableKeys = computed(() => {
418
+ const checked = this.targetCheckedSet();
419
+ return this.targetRowsBase()
420
+ .filter((r) => !r.disabled && checked.has(r.key))
421
+ .map((r) => r.key);
422
+ }, /* @ts-ignore */
423
+ ...(ngDevMode ? [{ debugName: "targetMovableKeys" }] : /* istanbul ignore next */ []));
424
+ sourceSelectAllKeys = computed(() => this.sourceRowsBase().filter((r) => !r.disabled).map((r) => r.key), /* @ts-ignore */
425
+ ...(ngDevMode ? [{ debugName: "sourceSelectAllKeys" }] : /* istanbul ignore next */ []));
426
+ targetSelectAllKeys = computed(() => this.targetRowsBase().filter((r) => !r.disabled).map((r) => r.key), /* @ts-ignore */
427
+ ...(ngDevMode ? [{ debugName: "targetSelectAllKeys" }] : /* istanbul ignore next */ []));
428
+ // ── Panel view-models ──
429
+ /** @internal */
430
+ sourcePanel = computed(() => this.buildPanel('source', this.sourceTitleId, this.resolvedLabels().sourceTitle, this.sourceRowsBase(), this.sourceRows(), this.sourceCheckedSet(), this.sourceSelectAllKeys(), this.sourceChecked(), this.sourceQuery()), /* @ts-ignore */
431
+ ...(ngDevMode ? [{ debugName: "sourcePanel" }] : /* istanbul ignore next */ []));
432
+ /** @internal */
433
+ targetPanel = computed(() => this.buildPanel('target', this.targetTitleId, this.resolvedLabels().targetTitle, this.targetRowsBase(), this.targetRows(), this.targetCheckedSet(), this.targetSelectAllKeys(), this.targetChecked(), this.targetQuery()), /* @ts-ignore */
434
+ ...(ngDevMode ? [{ debugName: "targetPanel" }] : /* istanbul ignore next */ []));
435
+ // ── tv() class outputs ──
436
+ variants = computed(() => transferVariants({
437
+ size: this.resolvedSize(),
438
+ disabled: this.disabled(),
439
+ }), /* @ts-ignore */
440
+ ...(ngDevMode ? [{ debugName: "variants" }] : /* istanbul ignore next */ []));
441
+ /** @internal */
442
+ rootClasses = computed(() => this.variants().root(), /* @ts-ignore */
443
+ ...(ngDevMode ? [{ debugName: "rootClasses" }] : /* istanbul ignore next */ []));
444
+ /** @internal */
445
+ panelClasses = computed(() => this.variants().panel(), /* @ts-ignore */
446
+ ...(ngDevMode ? [{ debugName: "panelClasses" }] : /* istanbul ignore next */ []));
447
+ /** @internal */
448
+ panelHeaderClasses = computed(() => this.variants().panelHeader(), /* @ts-ignore */
449
+ ...(ngDevMode ? [{ debugName: "panelHeaderClasses" }] : /* istanbul ignore next */ []));
450
+ /** @internal */
451
+ panelTitleClasses = computed(() => this.variants().panelTitle(), /* @ts-ignore */
452
+ ...(ngDevMode ? [{ debugName: "panelTitleClasses" }] : /* istanbul ignore next */ []));
453
+ /** @internal */
454
+ panelCountClasses = computed(() => this.variants().panelCount(), /* @ts-ignore */
455
+ ...(ngDevMode ? [{ debugName: "panelCountClasses" }] : /* istanbul ignore next */ []));
456
+ /** @internal */
457
+ searchClasses = computed(() => this.variants().search(), /* @ts-ignore */
458
+ ...(ngDevMode ? [{ debugName: "searchClasses" }] : /* istanbul ignore next */ []));
459
+ /** @internal */
460
+ listClasses = computed(() => this.variants().list(), /* @ts-ignore */
461
+ ...(ngDevMode ? [{ debugName: "listClasses" }] : /* istanbul ignore next */ []));
462
+ /** @internal */
463
+ optionLabelClasses = computed(() => this.variants().optionLabel(), /* @ts-ignore */
464
+ ...(ngDevMode ? [{ debugName: "optionLabelClasses" }] : /* istanbul ignore next */ []));
465
+ /** @internal */
466
+ controlsClasses = computed(() => this.variants().controls(), /* @ts-ignore */
467
+ ...(ngDevMode ? [{ debugName: "controlsClasses" }] : /* istanbul ignore next */ []));
468
+ /** @internal */
469
+ moveButtonClasses = computed(() => this.variants().moveButton(), /* @ts-ignore */
470
+ ...(ngDevMode ? [{ debugName: "moveButtonClasses" }] : /* istanbul ignore next */ []));
471
+ /** @internal */
472
+ moveIconClasses = computed(() => this.variants().moveIcon(), /* @ts-ignore */
473
+ ...(ngDevMode ? [{ debugName: "moveIconClasses" }] : /* istanbul ignore next */ []));
474
+ /** @internal */
475
+ emptyClasses = computed(() => this.variants().empty(), /* @ts-ignore */
476
+ ...(ngDevMode ? [{ debugName: "emptyClasses" }] : /* istanbul ignore next */ []));
477
+ constructor() {
478
+ super();
479
+ // Runtime CVA wiring — register on any host-level NgControl. The component
480
+ // injects `NgControl { self }` for TW_ERROR_STATE_MATCHER, so a static
481
+ // NG_VALUE_ACCESSOR provider would create circular DI on the same instance.
482
+ if (this.ngControl) {
483
+ this.ngControl.valueAccessor = this;
484
+ }
485
+ afterNextRender(() => {
486
+ if (isDevMode() && !this.hasAccessibleNameHint()) {
487
+ console.warn('[tw-transfer] The control has no accessible name. Provide `aria-label` / `aria-labelledby`, or wrap it in a <tw-form-field> with a <label twLabel>.');
488
+ }
489
+ });
490
+ }
491
+ // ── Public methods ──
492
+ /** Moves the given items (by key) to the target side; ignores keys already there or whose item is disabled. Clears the source panel's pending checks, emits `valueChange` + `moved`, calls the CVA onChange, and announces the move. No-op when the control is disabled. */
493
+ moveToTarget(keys) {
494
+ if (this.disabled())
495
+ return;
496
+ const current = this.targetKeys();
497
+ const existing = new Set(current);
498
+ const disabledFn = this.resolvedBehavior().disabledItem;
499
+ const map = this.itemByKey();
500
+ const moved = [];
501
+ for (const key of keys) {
502
+ if (existing.has(key))
503
+ continue;
504
+ const item = map.get(key);
505
+ if (item !== undefined && disabledFn(item))
506
+ continue;
507
+ existing.add(key);
508
+ moved.push(key);
509
+ }
510
+ this.sourceChecked.set([]);
511
+ if (!moved.length)
512
+ return;
513
+ // Append moved keys to the existing array — orphan keys (in the value, not in
514
+ // `data`) are part of `current` and therefore preserved.
515
+ this.applyValue([...current, ...moved], moved, 'toTarget');
516
+ }
517
+ /** Moves the given items (by key) to the source side; ignores keys not on target or whose item is disabled. Clears the target panel's pending checks. No-op when the control is disabled or `behavior.oneWay` is on. Same emissions as {@link moveToTarget}. */
518
+ moveToSource(keys) {
519
+ if (this.disabled() || this.resolvedBehavior().oneWay)
520
+ return;
521
+ const current = this.targetKeys();
522
+ const present = new Set(current);
523
+ const disabledFn = this.resolvedBehavior().disabledItem;
524
+ const map = this.itemByKey();
525
+ const remove = new Set();
526
+ for (const key of keys) {
527
+ if (!present.has(key))
528
+ continue;
529
+ const item = map.get(key);
530
+ if (item !== undefined && disabledFn(item))
531
+ continue;
532
+ remove.add(key);
533
+ }
534
+ this.targetChecked.set([]);
535
+ if (!remove.size)
536
+ return;
537
+ // Filter the existing array — unspecified orphan keys are preserved.
538
+ this.applyValue(current.filter((k) => !remove.has(k)), [...remove], 'toSource');
539
+ }
540
+ // ── Template event handlers ──
541
+ /** @internal */
542
+ onCheckedChange(side, event) {
543
+ if (side === 'source')
544
+ this.sourceChecked.set([...event.value]);
545
+ else
546
+ this.targetChecked.set([...event.value]);
547
+ }
548
+ /** @internal Select-all toggles the visible, enabled keys for the panel (replace semantics). */
549
+ onSelectAll(side, checked) {
550
+ if (side === 'source') {
551
+ this.sourceChecked.set(checked ? [...this.sourceSelectAllKeys()] : []);
552
+ }
553
+ else {
554
+ this.targetChecked.set(checked ? [...this.targetSelectAllKeys()] : []);
555
+ }
556
+ }
557
+ /** @internal */
558
+ onSearch(side, event) {
559
+ const value = event.target.value;
560
+ if (side === 'source')
561
+ this.sourceQuery.set(value);
562
+ else
563
+ this.targetQuery.set(value);
564
+ }
565
+ /** @internal */
566
+ onMoveToTarget() {
567
+ this.moveToTarget(this.sourceMovableKeys());
568
+ // The clicked → button disables itself once its checked set drains; a focused
569
+ // element that becomes `disabled` drops focus to <body>. Keep the user in the
570
+ // control by moving focus to the destination listbox (non-empty after a move).
571
+ this.focusDestination('target');
572
+ }
573
+ /** @internal */
574
+ onMoveToSource() {
575
+ this.moveToSource(this.targetMovableKeys());
576
+ this.focusDestination('source');
577
+ }
578
+ /**
579
+ * @internal Focuses the destination panel's listbox after the move re-renders.
580
+ * Deferred to the next render (the destination listbox may have just mounted
581
+ * from empty) and matched by its title id rather than DOM order (the *other*
582
+ * panel may have emptied, removing its listbox).
583
+ */
584
+ focusDestination(side) {
585
+ if (!this.platform.isBrowser)
586
+ return;
587
+ const titleId = side === 'source' ? this.sourceTitleId : this.targetTitleId;
588
+ afterNextRender(() => {
589
+ const listbox = this.elementRef.nativeElement.querySelector(`[cdklistbox][aria-labelledby="${titleId}"]`);
590
+ listbox?.focus();
591
+ }, { injector: this.injector });
592
+ }
593
+ // ── Internal helpers ──
594
+ applyValue(next, moved, direction) {
595
+ this.targetKeys.set(next);
596
+ this.onChange(next);
597
+ this.valueChange.emit(next);
598
+ this.moved.emit({ keys: moved, direction });
599
+ const labels = this.resolvedLabels();
600
+ const dest = direction === 'toTarget' ? labels.targetTitle : labels.sourceTitle;
601
+ this.liveAnnouncer.announce(formatLabel(labels.moveAnnouncement, { count: moved.length, target: dest }), 'polite');
602
+ }
603
+ filterItems(items, query) {
604
+ const q = query.trim();
605
+ if (!q)
606
+ return items;
607
+ const custom = this.resolvedBehavior().filterFn;
608
+ if (custom)
609
+ return items.filter((item) => custom(item, q));
610
+ const labelFn = this.labelFn();
611
+ const lower = q.toLowerCase();
612
+ return items.filter((item) => labelFn(item).toLowerCase().includes(lower));
613
+ }
614
+ toRowsBase(items) {
615
+ const kf = this.keyFn();
616
+ const labelFn = this.labelFn();
617
+ const disabledFn = this.resolvedBehavior().disabledItem;
618
+ return items.map((item) => ({
619
+ item,
620
+ key: kf(item),
621
+ label: labelFn(item),
622
+ disabled: disabledFn(item),
623
+ }));
624
+ }
625
+ toRows(base, checked, side) {
626
+ const size = this.resolvedSize();
627
+ return base.map((b) => {
628
+ const isChecked = checked.has(b.key);
629
+ const v = transferVariants({
630
+ size,
631
+ checked: isChecked,
632
+ optionDisabled: b.disabled,
633
+ });
634
+ return {
635
+ ...b,
636
+ checked: isChecked,
637
+ optionClass: v.option(),
638
+ checkClass: v.optionCheck(),
639
+ context: {
640
+ $implicit: b.item,
641
+ label: b.label,
642
+ checked: isChecked,
643
+ disabled: b.disabled,
644
+ side,
645
+ },
646
+ };
647
+ });
648
+ }
649
+ buildPanel(side, titleId, title, base, rows, checked, selectAllKeys, checkedValue, query) {
650
+ const labels = this.resolvedLabels();
651
+ const selectedCount = rows.reduce((n, r) => (r.checked ? n + 1 : n), 0);
652
+ const state = this.selectAllState(selectAllKeys, checked);
653
+ return {
654
+ side,
655
+ title,
656
+ titleId,
657
+ countText: formatLabel(labels.countFormat, {
658
+ total: base.length,
659
+ selected: selectedCount,
660
+ }),
661
+ emptyText: labels.emptyText,
662
+ searchPlaceholder: labels.searchPlaceholder,
663
+ selectAllLabel: labels.selectAllLabel,
664
+ query,
665
+ rows,
666
+ checkedValue,
667
+ selectAllChecked: state === 'checked',
668
+ selectAllIndeterminate: state === 'indeterminate',
669
+ };
670
+ }
671
+ selectAllState(enabledKeys, checked) {
672
+ if (enabledKeys.length === 0)
673
+ return 'unchecked';
674
+ let n = 0;
675
+ for (const key of enabledKeys)
676
+ if (checked.has(key))
677
+ n++;
678
+ if (n === 0)
679
+ return 'unchecked';
680
+ if (n === enabledKeys.length)
681
+ return 'checked';
682
+ return 'indeterminate';
683
+ }
684
+ hasAccessibleNameHint() {
685
+ return (!!this.ariaLabel() ||
686
+ !!this.ariaLabelledby() ||
687
+ this.labelledByIdsSignal().length > 0);
688
+ }
689
+ // ── FormFieldControl signals ──
690
+ /** @internal */
691
+ id = computed(() => this.idInput() ?? this.hostId, /* @ts-ignore */
692
+ ...(ngDevMode ? [{ debugName: "id" }] : /* istanbul ignore next */ []));
693
+ /** The current target-keys array. Doubles as the `FormFieldControl` value member. */
694
+ value = this.targetKeys.asReadonly();
695
+ /** @internal */
696
+ focused = this._focused.asReadonly();
697
+ /** @internal Empty when nothing is on the target side — drives the form-field floating label. */
698
+ empty = computed(() => this.targetKeys().length === 0, /* @ts-ignore */
699
+ ...(ngDevMode ? [{ debugName: "empty" }] : /* istanbul ignore next */ []));
700
+ /** @internal */
701
+ disabled = computed(() => this.disabledInput() || this.cvaDisabled(), /* @ts-ignore */
702
+ ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
703
+ /** @internal */
704
+ required = computed(() => {
705
+ this._ngControlRev();
706
+ if (this.requiredInput())
707
+ return true;
708
+ return !!this.ngControl?.control?.hasValidator(Validators.required);
709
+ }, /* @ts-ignore */
710
+ ...(ngDevMode ? [{ debugName: "required" }] : /* istanbul ignore next */ []));
711
+ /** @internal */
712
+ errorState = computed(() => {
713
+ this._ngControlRev();
714
+ this._formSubmitRev();
715
+ this._focused();
716
+ const matcher = this.errorStateMatcher() ?? this.defaultMatcher;
717
+ const form = this.parentFormGroup ??
718
+ this.parentForm;
719
+ return matcher.isErrorState(this.ngControl?.control ?? null, form);
720
+ }, /* @ts-ignore */
721
+ ...(ngDevMode ? [{ debugName: "errorState" }] : /* istanbul ignore next */ []));
722
+ /** @internal Active validation errors from the bound control, for `[twError match="…"]` filtering. */
723
+ errors = computed(() => {
724
+ this._ngControlRev();
725
+ return this.ngControl?.control?.errors ?? null;
726
+ }, /* @ts-ignore */
727
+ ...(ngDevMode ? [{ debugName: "errors" }] : /* istanbul ignore next */ []));
728
+ /** @internal */
729
+ controlType = 'transfer';
730
+ /** @internal */
731
+ userAriaDescribedBy = computed(() => this.ariaDescribedby(), /* @ts-ignore */
732
+ ...(ngDevMode ? [{ debugName: "userAriaDescribedBy" }] : /* istanbul ignore next */ []));
733
+ /** @internal */
734
+ userAriaLabelledby = computed(() => this.ariaLabelledby(), /* @ts-ignore */
735
+ ...(ngDevMode ? [{ debugName: "userAriaLabelledby" }] : /* istanbul ignore next */ []));
736
+ /** @internal Merged `aria-describedby` (form-field hint/error ids + consumer-supplied). */
737
+ resolvedDescribedBy = computed(() => {
738
+ const pushed = this.describedByIdsSignal();
739
+ if (pushed.length)
740
+ return pushed.join(' ');
741
+ const user = this.ariaDescribedby();
742
+ if (user)
743
+ return user.split(/\s+/).filter(Boolean).join(' ');
744
+ return '';
745
+ }, /* @ts-ignore */
746
+ ...(ngDevMode ? [{ debugName: "resolvedDescribedBy" }] : /* istanbul ignore next */ []));
747
+ /** @internal Merged `aria-labelledby` (form-field label id + consumer-supplied). */
748
+ resolvedLabelledBy = computed(() => {
749
+ const pushed = this.labelledByIdsSignal();
750
+ if (pushed.length)
751
+ return pushed.join(' ');
752
+ const user = this.ariaLabelledby();
753
+ if (user)
754
+ return user.split(/\s+/).filter(Boolean).join(' ');
755
+ return '';
756
+ }, /* @ts-ignore */
757
+ ...(ngDevMode ? [{ debugName: "resolvedLabelledBy" }] : /* istanbul ignore next */ []));
758
+ // ── ControlValueAccessor ──
759
+ writeValue(value) {
760
+ // Orphan keys (present here but absent from `data`) are preserved — they render
761
+ // nothing but survive the round-trip, so a form value outlives a lagging `data`.
762
+ this.targetKeys.set(Array.isArray(value) ? [...value] : []);
763
+ this.sourceChecked.set([]);
764
+ this.targetChecked.set([]);
765
+ }
766
+ registerOnChange(fn) {
767
+ this.onChange = fn;
768
+ }
769
+ registerOnTouched(fn) {
770
+ this.onTouched = fn;
771
+ }
772
+ setDisabledState(isDisabled) {
773
+ this.cvaDisabled.set(isDisabled);
774
+ }
775
+ // ── FormFieldControl methods ──
776
+ /** @internal */
777
+ setDescribedByIds(ids) {
778
+ this.describedByIdsSignal.set([...ids]);
779
+ }
780
+ /** @internal */
781
+ setLabelledByIds(ids) {
782
+ this.labelledByIdsSignal.set([...ids]);
783
+ }
784
+ /** @internal Clicking the form-field surface focuses the first listbox. */
785
+ onContainerClick(event) {
786
+ if (this.disabled() || event.defaultPrevented)
787
+ return;
788
+ const host = this.elementRef.nativeElement;
789
+ if (event.target !== host && host.contains(event.target))
790
+ return;
791
+ const listbox = host.querySelector('[cdklistbox]');
792
+ listbox?.focus();
793
+ }
794
+ // ── Lifecycle ──
795
+ ngOnInit() {
796
+ // Host-level focus monitor (checkChildren: true): internal focus moves between
797
+ // the two listboxes / buttons / search stay "focused"; origin === null fires
798
+ // only when focus leaves the WHOLE control — the single source for onTouched.
799
+ this.focusMonitor
800
+ .monitor(this.elementRef, true)
801
+ .pipe(takeUntilDestroyed(this.destroyRef))
802
+ .subscribe((origin) => {
803
+ const wasFocused = this._focused();
804
+ this._focused.set(!!origin);
805
+ if (wasFocused && !origin) {
806
+ this.onTouched();
807
+ this._ngControlRev.update((n) => n + 1);
808
+ }
809
+ });
810
+ this.destroyRef.onDestroy(() => this.focusMonitor.stopMonitoring(this.elementRef));
811
+ // React to status/value changes on the bound control so errorState recomputes.
812
+ const ctrl = this.ngControl?.control;
813
+ if (ctrl) {
814
+ const streams = [ctrl.statusChanges, ctrl.valueChanges].filter((s) => !!s);
815
+ if (streams.length) {
816
+ merge(...streams)
817
+ .pipe(takeUntilDestroyed(this.destroyRef))
818
+ .subscribe(() => this._ngControlRev.update((n) => n + 1));
819
+ }
820
+ }
821
+ const submit = this.parentFormGroup?.ngSubmit ?? this.parentForm?.ngSubmit;
822
+ if (submit) {
823
+ submit
824
+ .pipe(takeUntilDestroyed(this.destroyRef))
825
+ .subscribe(() => this._formSubmitRev.update((n) => n + 1));
826
+ }
827
+ }
828
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: TransferComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
829
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: TransferComponent, isStandalone: true, selector: "tw-transfer", inputs: { data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: false, transformFunction: null }, keyFn: { classPropertyName: "keyFn", publicName: "keyFn", isSignal: true, isRequired: true, transformFunction: null }, labelFn: { classPropertyName: "labelFn", publicName: "labelFn", isSignal: true, isRequired: false, transformFunction: null }, labels: { classPropertyName: "labels", publicName: "labels", isSignal: true, isRequired: false, transformFunction: null }, display: { classPropertyName: "display", publicName: "display", isSignal: true, isRequired: false, transformFunction: null }, behavior: { classPropertyName: "behavior", publicName: "behavior", isSignal: true, isRequired: false, transformFunction: null }, disabledInput: { classPropertyName: "disabledInput", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, requiredInput: { classPropertyName: "requiredInput", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null }, idInput: { classPropertyName: "idInput", publicName: "id", 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 }, ariaDescribedby: { classPropertyName: "ariaDescribedby", publicName: "aria-describedby", isSignal: true, isRequired: false, transformFunction: null }, errorStateMatcher: { classPropertyName: "errorStateMatcher", publicName: "errorStateMatcher", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { valueChange: "valueChange", moved: "moved" }, host: { attributes: { "role": "group" }, properties: { "id": "id()", "class": "rootClasses()", "attr.aria-label": "ariaLabel() || null", "attr.aria-labelledby": "resolvedLabelledBy() || null", "attr.aria-describedby": "resolvedDescribedBy() || null", "attr.aria-disabled": "disabled() || null", "attr.aria-invalid": "errorState() || null", "attr.data-focused": "focused() || null" } }, providers: [
830
+ {
831
+ provide: TW_FORM_FIELD_CONTROL,
832
+ useExisting: forwardRef(() => TransferComponent),
833
+ },
834
+ ], queries: [{ propertyName: "itemTemplate", first: true, predicate: (TransferItemDefDirective), descendants: true, isSignal: true }], exportAs: ["twTransfer"], usesInheritance: true, ngImport: i0, template: `
835
+ <!-- One panel, instantiated twice (source, then target) around the controls. -->
836
+ <ng-template #panelTpl let-panel>
837
+ <div [class]="panelClasses()">
838
+ <div [class]="panelHeaderClasses()">
839
+ @if (showSelectAll()) {
840
+ <tw-checkbox
841
+ [size]="checkboxSize()"
842
+ [checked]="panel.selectAllChecked"
843
+ [indeterminate]="panel.selectAllIndeterminate"
844
+ [disabled]="disabled()"
845
+ [aria-label]="panel.selectAllLabel"
846
+ (change)="onSelectAll(panel.side, $event)"
847
+ />
848
+ }
849
+ <span [id]="panel.titleId" [class]="panelTitleClasses()">{{ panel.title }}</span>
850
+ <span [class]="panelCountClasses()">{{ panel.countText }}</span>
851
+ </div>
852
+
853
+ @if (showSearch()) {
854
+ <div [class]="searchClasses()">
855
+ <input
856
+ twInput
857
+ type="text"
858
+ [size]="resolvedSize()"
859
+ [value]="panel.query"
860
+ [disabled]="disabled()"
861
+ [attr.placeholder]="panel.searchPlaceholder"
862
+ [attr.aria-label]="panel.searchPlaceholder"
863
+ (input)="onSearch(panel.side, $event)"
864
+ />
865
+ </div>
866
+ }
867
+
868
+ <!-- The cdkListbox is rendered only when it has options. An empty
869
+ role="listbox" violates aria-required-children, so an empty panel
870
+ shows a plain message region instead. -->
871
+ @if (panel.rows.length > 0) {
872
+ <div
873
+ cdkListbox
874
+ [cdkListboxMultiple]="true"
875
+ [cdkListboxValue]="panel.checkedValue"
876
+ [cdkListboxDisabled]="disabled()"
877
+ [cdkListboxCompareWith]="compareKeys"
878
+ [class]="listClasses()"
879
+ [style.height.px]="listHeightPx()"
880
+ [attr.aria-labelledby]="panel.titleId"
881
+ (cdkListboxValueChange)="onCheckedChange(panel.side, $event)"
882
+ >
883
+ @for (row of panel.rows; track row.key) {
884
+ <div
885
+ [cdkOption]="row.key"
886
+ [cdkOptionDisabled]="row.disabled"
887
+ [cdkOptionTypeaheadLabel]="row.label"
888
+ [class]="row.optionClass"
889
+ >
890
+ <svg
891
+ viewBox="0 0 20 20"
892
+ fill="currentColor"
893
+ aria-hidden="true"
894
+ [class]="row.checkClass"
895
+ >
896
+ <path
897
+ fill-rule="evenodd"
898
+ d="M16.704 5.29a1 1 0 0 1 .006 1.414l-7.5 7.6a1 1 0 0 1-1.42.005l-3.5-3.5a1 1 0 1 1 1.414-1.414l2.79 2.79 6.796-6.889a1 1 0 0 1 1.414-.006Z"
899
+ clip-rule="evenodd"
900
+ />
901
+ </svg>
902
+ @if (itemTemplate(); as tpl) {
903
+ <ng-container
904
+ [ngTemplateOutlet]="tpl.template"
905
+ [ngTemplateOutletContext]="row.context"
906
+ />
907
+ } @else {
908
+ <span [class]="optionLabelClasses()">{{ row.label }}</span>
909
+ }
910
+ </div>
911
+ }
912
+ </div>
913
+ } @else {
914
+ <div [class]="emptyClasses()" [style.height.px]="listHeightPx()">{{ panel.emptyText }}</div>
915
+ }
916
+ </div>
917
+ </ng-template>
918
+
919
+ <ng-container
920
+ [ngTemplateOutlet]="panelTpl"
921
+ [ngTemplateOutletContext]="{ $implicit: sourcePanel() }"
922
+ />
923
+
924
+ <div [class]="controlsClasses()">
925
+ <button
926
+ type="button"
927
+ [class]="moveButtonClasses()"
928
+ [disabled]="disabled() || sourceMovableKeys().length === 0"
929
+ [attr.aria-label]="resolvedLabels().moveToTargetLabel"
930
+ (click)="onMoveToTarget()"
931
+ >
932
+ <svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" [class]="moveIconClasses()">
933
+ <path
934
+ fill-rule="evenodd"
935
+ d="M7.21 14.77a.75.75 0 0 1 .02-1.06L11.168 10 7.23 6.29a.75.75 0 1 1 1.04-1.08l4.5 4.25a.75.75 0 0 1 0 1.08l-4.5 4.25a.75.75 0 0 1-1.06-.02Z"
936
+ clip-rule="evenodd"
937
+ />
938
+ </svg>
939
+ </button>
940
+
941
+ @if (!oneWay()) {
942
+ <button
943
+ type="button"
944
+ [class]="moveButtonClasses()"
945
+ [disabled]="disabled() || targetMovableKeys().length === 0"
946
+ [attr.aria-label]="resolvedLabels().moveToSourceLabel"
947
+ (click)="onMoveToSource()"
948
+ >
949
+ <svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" [class]="moveIconClasses()">
950
+ <path
951
+ fill-rule="evenodd"
952
+ d="M12.79 5.23a.75.75 0 0 1-.02 1.06L8.832 10l3.938 3.71a.75.75 0 1 1-1.04 1.08l-4.5-4.25a.75.75 0 0 1 0-1.08l4.5-4.25a.75.75 0 0 1 1.06.02Z"
953
+ clip-rule="evenodd"
954
+ />
955
+ </svg>
956
+ </button>
957
+ }
958
+ </div>
959
+
960
+ <ng-container
961
+ [ngTemplateOutlet]="panelTpl"
962
+ [ngTemplateOutletContext]="{ $implicit: targetPanel() }"
963
+ />
964
+ `, isInline: true, dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: CdkListbox, selector: "[cdkListbox]", inputs: ["id", "tabindex", "cdkListboxValue", "cdkListboxMultiple", "cdkListboxDisabled", "cdkListboxUseActiveDescendant", "cdkListboxOrientation", "cdkListboxCompareWith", "cdkListboxNavigationWrapDisabled", "cdkListboxNavigatesDisabledOptions"], outputs: ["cdkListboxValueChange"], exportAs: ["cdkListbox"] }, { kind: "directive", type: CdkOption, selector: "[cdkOption]", inputs: ["id", "cdkOption", "cdkOptionTypeaheadLabel", "cdkOptionDisabled", "tabindex"], exportAs: ["cdkOption"] }, { 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"] }, { kind: "directive", type: InputDirective, selector: "input[twInput], textarea[twInput]", inputs: ["id", "type", "size", "disabled", "required", "readonly", "errorStateMatcher", "aria-describedby", "aria-labelledby"], exportAs: ["twInput"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
965
+ }
966
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: TransferComponent, decorators: [{
967
+ type: Component,
968
+ args: [{
969
+ selector: 'tw-transfer',
970
+ exportAs: 'twTransfer',
971
+ changeDetection: ChangeDetectionStrategy.OnPush,
972
+ imports: [NgTemplateOutlet, CdkListbox, CdkOption, CheckboxComponent, InputDirective],
973
+ providers: [
974
+ {
975
+ provide: TW_FORM_FIELD_CONTROL,
976
+ useExisting: forwardRef(() => TransferComponent),
977
+ },
978
+ ],
979
+ host: {
980
+ role: 'group',
981
+ '[id]': 'id()',
982
+ '[class]': 'rootClasses()',
983
+ '[attr.aria-label]': 'ariaLabel() || null',
984
+ '[attr.aria-labelledby]': 'resolvedLabelledBy() || null',
985
+ '[attr.aria-describedby]': 'resolvedDescribedBy() || null',
986
+ '[attr.aria-disabled]': 'disabled() || null',
987
+ '[attr.aria-invalid]': 'errorState() || null',
988
+ '[attr.data-focused]': 'focused() || null',
989
+ },
990
+ template: `
991
+ <!-- One panel, instantiated twice (source, then target) around the controls. -->
992
+ <ng-template #panelTpl let-panel>
993
+ <div [class]="panelClasses()">
994
+ <div [class]="panelHeaderClasses()">
995
+ @if (showSelectAll()) {
996
+ <tw-checkbox
997
+ [size]="checkboxSize()"
998
+ [checked]="panel.selectAllChecked"
999
+ [indeterminate]="panel.selectAllIndeterminate"
1000
+ [disabled]="disabled()"
1001
+ [aria-label]="panel.selectAllLabel"
1002
+ (change)="onSelectAll(panel.side, $event)"
1003
+ />
1004
+ }
1005
+ <span [id]="panel.titleId" [class]="panelTitleClasses()">{{ panel.title }}</span>
1006
+ <span [class]="panelCountClasses()">{{ panel.countText }}</span>
1007
+ </div>
1008
+
1009
+ @if (showSearch()) {
1010
+ <div [class]="searchClasses()">
1011
+ <input
1012
+ twInput
1013
+ type="text"
1014
+ [size]="resolvedSize()"
1015
+ [value]="panel.query"
1016
+ [disabled]="disabled()"
1017
+ [attr.placeholder]="panel.searchPlaceholder"
1018
+ [attr.aria-label]="panel.searchPlaceholder"
1019
+ (input)="onSearch(panel.side, $event)"
1020
+ />
1021
+ </div>
1022
+ }
1023
+
1024
+ <!-- The cdkListbox is rendered only when it has options. An empty
1025
+ role="listbox" violates aria-required-children, so an empty panel
1026
+ shows a plain message region instead. -->
1027
+ @if (panel.rows.length > 0) {
1028
+ <div
1029
+ cdkListbox
1030
+ [cdkListboxMultiple]="true"
1031
+ [cdkListboxValue]="panel.checkedValue"
1032
+ [cdkListboxDisabled]="disabled()"
1033
+ [cdkListboxCompareWith]="compareKeys"
1034
+ [class]="listClasses()"
1035
+ [style.height.px]="listHeightPx()"
1036
+ [attr.aria-labelledby]="panel.titleId"
1037
+ (cdkListboxValueChange)="onCheckedChange(panel.side, $event)"
1038
+ >
1039
+ @for (row of panel.rows; track row.key) {
1040
+ <div
1041
+ [cdkOption]="row.key"
1042
+ [cdkOptionDisabled]="row.disabled"
1043
+ [cdkOptionTypeaheadLabel]="row.label"
1044
+ [class]="row.optionClass"
1045
+ >
1046
+ <svg
1047
+ viewBox="0 0 20 20"
1048
+ fill="currentColor"
1049
+ aria-hidden="true"
1050
+ [class]="row.checkClass"
1051
+ >
1052
+ <path
1053
+ fill-rule="evenodd"
1054
+ d="M16.704 5.29a1 1 0 0 1 .006 1.414l-7.5 7.6a1 1 0 0 1-1.42.005l-3.5-3.5a1 1 0 1 1 1.414-1.414l2.79 2.79 6.796-6.889a1 1 0 0 1 1.414-.006Z"
1055
+ clip-rule="evenodd"
1056
+ />
1057
+ </svg>
1058
+ @if (itemTemplate(); as tpl) {
1059
+ <ng-container
1060
+ [ngTemplateOutlet]="tpl.template"
1061
+ [ngTemplateOutletContext]="row.context"
1062
+ />
1063
+ } @else {
1064
+ <span [class]="optionLabelClasses()">{{ row.label }}</span>
1065
+ }
1066
+ </div>
1067
+ }
1068
+ </div>
1069
+ } @else {
1070
+ <div [class]="emptyClasses()" [style.height.px]="listHeightPx()">{{ panel.emptyText }}</div>
1071
+ }
1072
+ </div>
1073
+ </ng-template>
1074
+
1075
+ <ng-container
1076
+ [ngTemplateOutlet]="panelTpl"
1077
+ [ngTemplateOutletContext]="{ $implicit: sourcePanel() }"
1078
+ />
1079
+
1080
+ <div [class]="controlsClasses()">
1081
+ <button
1082
+ type="button"
1083
+ [class]="moveButtonClasses()"
1084
+ [disabled]="disabled() || sourceMovableKeys().length === 0"
1085
+ [attr.aria-label]="resolvedLabels().moveToTargetLabel"
1086
+ (click)="onMoveToTarget()"
1087
+ >
1088
+ <svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" [class]="moveIconClasses()">
1089
+ <path
1090
+ fill-rule="evenodd"
1091
+ d="M7.21 14.77a.75.75 0 0 1 .02-1.06L11.168 10 7.23 6.29a.75.75 0 1 1 1.04-1.08l4.5 4.25a.75.75 0 0 1 0 1.08l-4.5 4.25a.75.75 0 0 1-1.06-.02Z"
1092
+ clip-rule="evenodd"
1093
+ />
1094
+ </svg>
1095
+ </button>
1096
+
1097
+ @if (!oneWay()) {
1098
+ <button
1099
+ type="button"
1100
+ [class]="moveButtonClasses()"
1101
+ [disabled]="disabled() || targetMovableKeys().length === 0"
1102
+ [attr.aria-label]="resolvedLabels().moveToSourceLabel"
1103
+ (click)="onMoveToSource()"
1104
+ >
1105
+ <svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" [class]="moveIconClasses()">
1106
+ <path
1107
+ fill-rule="evenodd"
1108
+ d="M12.79 5.23a.75.75 0 0 1-.02 1.06L8.832 10l3.938 3.71a.75.75 0 1 1-1.04 1.08l-4.5-4.25a.75.75 0 0 1 0-1.08l4.5-4.25a.75.75 0 0 1 1.06.02Z"
1109
+ clip-rule="evenodd"
1110
+ />
1111
+ </svg>
1112
+ </button>
1113
+ }
1114
+ </div>
1115
+
1116
+ <ng-container
1117
+ [ngTemplateOutlet]="panelTpl"
1118
+ [ngTemplateOutletContext]="{ $implicit: targetPanel() }"
1119
+ />
1120
+ `,
1121
+ }]
1122
+ }], ctorParameters: () => [], propDecorators: { data: [{ type: i0.Input, args: [{ isSignal: true, alias: "data", required: false }] }], keyFn: [{ type: i0.Input, args: [{ isSignal: true, alias: "keyFn", required: true }] }], labelFn: [{ type: i0.Input, args: [{ isSignal: true, alias: "labelFn", required: false }] }], labels: [{ type: i0.Input, args: [{ isSignal: true, alias: "labels", required: false }] }], display: [{ type: i0.Input, args: [{ isSignal: true, alias: "display", required: false }] }], behavior: [{ type: i0.Input, args: [{ isSignal: true, alias: "behavior", required: false }] }], disabledInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], requiredInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: false }] }], idInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", 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 }] }], ariaDescribedby: [{ type: i0.Input, args: [{ isSignal: true, alias: "aria-describedby", required: false }] }], errorStateMatcher: [{ type: i0.Input, args: [{ isSignal: true, alias: "errorStateMatcher", required: false }] }], valueChange: [{ type: i0.Output, args: ["valueChange"] }], moved: [{ type: i0.Output, args: ["moved"] }], itemTemplate: [{ type: i0.ContentChild, args: [i0.forwardRef(() => TransferItemDefDirective), { isSignal: true }] }] } });
1123
+
1124
+ /**
1125
+ * Generated bundle index. Do not edit.
1126
+ */
1127
+
1128
+ export { TransferComponent, TransferItemDefDirective };
1129
+ //# sourceMappingURL=cdevhub-ngx-tw-transfer.mjs.map