@cdevhub/ngx-tw 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +134 -0
  3. package/fesm2022/cdevhub-ngx-tw-accordion.mjs +108 -0
  4. package/fesm2022/cdevhub-ngx-tw-accordion.mjs.map +1 -0
  5. package/fesm2022/cdevhub-ngx-tw-alert.mjs +240 -0
  6. package/fesm2022/cdevhub-ngx-tw-alert.mjs.map +1 -0
  7. package/fesm2022/cdevhub-ngx-tw-avatar.mjs +287 -0
  8. package/fesm2022/cdevhub-ngx-tw-avatar.mjs.map +1 -0
  9. package/fesm2022/cdevhub-ngx-tw-badge.mjs +332 -0
  10. package/fesm2022/cdevhub-ngx-tw-badge.mjs.map +1 -0
  11. package/fesm2022/cdevhub-ngx-tw-breadcrumbs.mjs +452 -0
  12. package/fesm2022/cdevhub-ngx-tw-breadcrumbs.mjs.map +1 -0
  13. package/fesm2022/cdevhub-ngx-tw-button.mjs +227 -0
  14. package/fesm2022/cdevhub-ngx-tw-button.mjs.map +1 -0
  15. package/fesm2022/cdevhub-ngx-tw-calendar-luxon.mjs +417 -0
  16. package/fesm2022/cdevhub-ngx-tw-calendar-luxon.mjs.map +1 -0
  17. package/fesm2022/cdevhub-ngx-tw-calendar-testing.mjs +232 -0
  18. package/fesm2022/cdevhub-ngx-tw-calendar-testing.mjs.map +1 -0
  19. package/fesm2022/cdevhub-ngx-tw-calendar.mjs +4482 -0
  20. package/fesm2022/cdevhub-ngx-tw-calendar.mjs.map +1 -0
  21. package/fesm2022/cdevhub-ngx-tw-card.mjs +157 -0
  22. package/fesm2022/cdevhub-ngx-tw-card.mjs.map +1 -0
  23. package/fesm2022/cdevhub-ngx-tw-carousel.mjs +1454 -0
  24. package/fesm2022/cdevhub-ngx-tw-carousel.mjs.map +1 -0
  25. package/fesm2022/cdevhub-ngx-tw-checkbox.mjs +593 -0
  26. package/fesm2022/cdevhub-ngx-tw-checkbox.mjs.map +1 -0
  27. package/fesm2022/cdevhub-ngx-tw-code-block.mjs +228 -0
  28. package/fesm2022/cdevhub-ngx-tw-code-block.mjs.map +1 -0
  29. package/fesm2022/cdevhub-ngx-tw-collapsible.mjs +510 -0
  30. package/fesm2022/cdevhub-ngx-tw-collapsible.mjs.map +1 -0
  31. package/fesm2022/cdevhub-ngx-tw-combobox.mjs +1721 -0
  32. package/fesm2022/cdevhub-ngx-tw-combobox.mjs.map +1 -0
  33. package/fesm2022/cdevhub-ngx-tw-command-palette.mjs +721 -0
  34. package/fesm2022/cdevhub-ngx-tw-command-palette.mjs.map +1 -0
  35. package/fesm2022/cdevhub-ngx-tw-core.mjs +861 -0
  36. package/fesm2022/cdevhub-ngx-tw-core.mjs.map +1 -0
  37. package/fesm2022/cdevhub-ngx-tw-date-picker.mjs +1558 -0
  38. package/fesm2022/cdevhub-ngx-tw-date-picker.mjs.map +1 -0
  39. package/fesm2022/cdevhub-ngx-tw-date-range-picker.mjs +1833 -0
  40. package/fesm2022/cdevhub-ngx-tw-date-range-picker.mjs.map +1 -0
  41. package/fesm2022/cdevhub-ngx-tw-dialog.mjs +693 -0
  42. package/fesm2022/cdevhub-ngx-tw-dialog.mjs.map +1 -0
  43. package/fesm2022/cdevhub-ngx-tw-empty-state.mjs +331 -0
  44. package/fesm2022/cdevhub-ngx-tw-empty-state.mjs.map +1 -0
  45. package/fesm2022/cdevhub-ngx-tw-flip-card.mjs +289 -0
  46. package/fesm2022/cdevhub-ngx-tw-flip-card.mjs.map +1 -0
  47. package/fesm2022/cdevhub-ngx-tw-form-field.mjs +572 -0
  48. package/fesm2022/cdevhub-ngx-tw-form-field.mjs.map +1 -0
  49. package/fesm2022/cdevhub-ngx-tw-icon-lucide.mjs +36 -0
  50. package/fesm2022/cdevhub-ngx-tw-icon-lucide.mjs.map +1 -0
  51. package/fesm2022/cdevhub-ngx-tw-icon.mjs +278 -0
  52. package/fesm2022/cdevhub-ngx-tw-icon.mjs.map +1 -0
  53. package/fesm2022/cdevhub-ngx-tw-input.mjs +355 -0
  54. package/fesm2022/cdevhub-ngx-tw-input.mjs.map +1 -0
  55. package/fesm2022/cdevhub-ngx-tw-item.mjs +241 -0
  56. package/fesm2022/cdevhub-ngx-tw-item.mjs.map +1 -0
  57. package/fesm2022/cdevhub-ngx-tw-menu.mjs +433 -0
  58. package/fesm2022/cdevhub-ngx-tw-menu.mjs.map +1 -0
  59. package/fesm2022/cdevhub-ngx-tw-paginator.mjs +691 -0
  60. package/fesm2022/cdevhub-ngx-tw-paginator.mjs.map +1 -0
  61. package/fesm2022/cdevhub-ngx-tw-popover.mjs +678 -0
  62. package/fesm2022/cdevhub-ngx-tw-popover.mjs.map +1 -0
  63. package/fesm2022/cdevhub-ngx-tw-progress-bar.mjs +335 -0
  64. package/fesm2022/cdevhub-ngx-tw-progress-bar.mjs.map +1 -0
  65. package/fesm2022/cdevhub-ngx-tw-radio.mjs +708 -0
  66. package/fesm2022/cdevhub-ngx-tw-radio.mjs.map +1 -0
  67. package/fesm2022/cdevhub-ngx-tw-segmented-control.mjs +319 -0
  68. package/fesm2022/cdevhub-ngx-tw-segmented-control.mjs.map +1 -0
  69. package/fesm2022/cdevhub-ngx-tw-select.mjs +1722 -0
  70. package/fesm2022/cdevhub-ngx-tw-select.mjs.map +1 -0
  71. package/fesm2022/cdevhub-ngx-tw-separator.mjs +115 -0
  72. package/fesm2022/cdevhub-ngx-tw-separator.mjs.map +1 -0
  73. package/fesm2022/cdevhub-ngx-tw-sheet.mjs +768 -0
  74. package/fesm2022/cdevhub-ngx-tw-sheet.mjs.map +1 -0
  75. package/fesm2022/cdevhub-ngx-tw-skeleton.mjs +149 -0
  76. package/fesm2022/cdevhub-ngx-tw-skeleton.mjs.map +1 -0
  77. package/fesm2022/cdevhub-ngx-tw-slider.mjs +1147 -0
  78. package/fesm2022/cdevhub-ngx-tw-slider.mjs.map +1 -0
  79. package/fesm2022/cdevhub-ngx-tw-sort.mjs +346 -0
  80. package/fesm2022/cdevhub-ngx-tw-sort.mjs.map +1 -0
  81. package/fesm2022/cdevhub-ngx-tw-spinner.mjs +186 -0
  82. package/fesm2022/cdevhub-ngx-tw-spinner.mjs.map +1 -0
  83. package/fesm2022/cdevhub-ngx-tw-split.mjs +1082 -0
  84. package/fesm2022/cdevhub-ngx-tw-split.mjs.map +1 -0
  85. package/fesm2022/cdevhub-ngx-tw-stat.mjs +579 -0
  86. package/fesm2022/cdevhub-ngx-tw-stat.mjs.map +1 -0
  87. package/fesm2022/cdevhub-ngx-tw-stepper.mjs +443 -0
  88. package/fesm2022/cdevhub-ngx-tw-stepper.mjs.map +1 -0
  89. package/fesm2022/cdevhub-ngx-tw-switch.mjs +391 -0
  90. package/fesm2022/cdevhub-ngx-tw-switch.mjs.map +1 -0
  91. package/fesm2022/cdevhub-ngx-tw-tab-nav.mjs +315 -0
  92. package/fesm2022/cdevhub-ngx-tw-tab-nav.mjs.map +1 -0
  93. package/fesm2022/cdevhub-ngx-tw-table.mjs +994 -0
  94. package/fesm2022/cdevhub-ngx-tw-table.mjs.map +1 -0
  95. package/fesm2022/cdevhub-ngx-tw-tabs.mjs +508 -0
  96. package/fesm2022/cdevhub-ngx-tw-tabs.mjs.map +1 -0
  97. package/fesm2022/cdevhub-ngx-tw-textarea.mjs +147 -0
  98. package/fesm2022/cdevhub-ngx-tw-textarea.mjs.map +1 -0
  99. package/fesm2022/cdevhub-ngx-tw-theme.mjs +178 -0
  100. package/fesm2022/cdevhub-ngx-tw-theme.mjs.map +1 -0
  101. package/fesm2022/cdevhub-ngx-tw-time-picker.mjs +1344 -0
  102. package/fesm2022/cdevhub-ngx-tw-time-picker.mjs.map +1 -0
  103. package/fesm2022/cdevhub-ngx-tw-timeline.mjs +1136 -0
  104. package/fesm2022/cdevhub-ngx-tw-timeline.mjs.map +1 -0
  105. package/fesm2022/cdevhub-ngx-tw-toast.mjs +1311 -0
  106. package/fesm2022/cdevhub-ngx-tw-toast.mjs.map +1 -0
  107. package/fesm2022/cdevhub-ngx-tw-tooltip.mjs +533 -0
  108. package/fesm2022/cdevhub-ngx-tw-tooltip.mjs.map +1 -0
  109. package/fesm2022/cdevhub-ngx-tw.mjs +61 -0
  110. package/fesm2022/cdevhub-ngx-tw.mjs.map +1 -0
  111. package/package.json +265 -0
  112. package/theme/_base.css +394 -0
  113. package/theme/_dark.css +510 -0
  114. package/theme/_high-contrast.css +290 -0
  115. package/theme/_semantic.css +317 -0
  116. package/theme/_typography.css +9 -0
  117. package/theme/index.css +54 -0
  118. package/types/cdevhub-ngx-tw-accordion.d.ts +48 -0
  119. package/types/cdevhub-ngx-tw-alert.d.ts +67 -0
  120. package/types/cdevhub-ngx-tw-avatar.d.ts +79 -0
  121. package/types/cdevhub-ngx-tw-badge.d.ts +94 -0
  122. package/types/cdevhub-ngx-tw-breadcrumbs.d.ts +227 -0
  123. package/types/cdevhub-ngx-tw-button.d.ts +74 -0
  124. package/types/cdevhub-ngx-tw-calendar-luxon.d.ts +200 -0
  125. package/types/cdevhub-ngx-tw-calendar-testing.d.ts +113 -0
  126. package/types/cdevhub-ngx-tw-calendar.d.ts +1704 -0
  127. package/types/cdevhub-ngx-tw-card.d.ts +48 -0
  128. package/types/cdevhub-ngx-tw-carousel.d.ts +406 -0
  129. package/types/cdevhub-ngx-tw-checkbox.d.ts +136 -0
  130. package/types/cdevhub-ngx-tw-code-block.d.ts +93 -0
  131. package/types/cdevhub-ngx-tw-collapsible.d.ts +151 -0
  132. package/types/cdevhub-ngx-tw-combobox.d.ts +389 -0
  133. package/types/cdevhub-ngx-tw-command-palette.d.ts +215 -0
  134. package/types/cdevhub-ngx-tw-core.d.ts +719 -0
  135. package/types/cdevhub-ngx-tw-date-picker.d.ts +317 -0
  136. package/types/cdevhub-ngx-tw-date-range-picker.d.ts +313 -0
  137. package/types/cdevhub-ngx-tw-dialog.d.ts +303 -0
  138. package/types/cdevhub-ngx-tw-empty-state.d.ts +104 -0
  139. package/types/cdevhub-ngx-tw-flip-card.d.ts +95 -0
  140. package/types/cdevhub-ngx-tw-form-field.d.ts +210 -0
  141. package/types/cdevhub-ngx-tw-icon-lucide.d.ts +29 -0
  142. package/types/cdevhub-ngx-tw-icon.d.ts +134 -0
  143. package/types/cdevhub-ngx-tw-input.d.ts +110 -0
  144. package/types/cdevhub-ngx-tw-item.d.ts +70 -0
  145. package/types/cdevhub-ngx-tw-menu.d.ts +99 -0
  146. package/types/cdevhub-ngx-tw-paginator.d.ts +305 -0
  147. package/types/cdevhub-ngx-tw-popover.d.ts +157 -0
  148. package/types/cdevhub-ngx-tw-progress-bar.d.ts +100 -0
  149. package/types/cdevhub-ngx-tw-radio.d.ts +174 -0
  150. package/types/cdevhub-ngx-tw-segmented-control.d.ts +66 -0
  151. package/types/cdevhub-ngx-tw-select.d.ts +384 -0
  152. package/types/cdevhub-ngx-tw-separator.d.ts +28 -0
  153. package/types/cdevhub-ngx-tw-sheet.d.ts +335 -0
  154. package/types/cdevhub-ngx-tw-skeleton.d.ts +52 -0
  155. package/types/cdevhub-ngx-tw-slider.d.ts +258 -0
  156. package/types/cdevhub-ngx-tw-sort.d.ts +136 -0
  157. package/types/cdevhub-ngx-tw-spinner.d.ts +50 -0
  158. package/types/cdevhub-ngx-tw-split.d.ts +265 -0
  159. package/types/cdevhub-ngx-tw-stat.d.ts +140 -0
  160. package/types/cdevhub-ngx-tw-stepper.d.ts +127 -0
  161. package/types/cdevhub-ngx-tw-switch.d.ts +88 -0
  162. package/types/cdevhub-ngx-tw-tab-nav.d.ts +107 -0
  163. package/types/cdevhub-ngx-tw-table.d.ts +479 -0
  164. package/types/cdevhub-ngx-tw-tabs.d.ts +121 -0
  165. package/types/cdevhub-ngx-tw-textarea.d.ts +62 -0
  166. package/types/cdevhub-ngx-tw-theme.d.ts +106 -0
  167. package/types/cdevhub-ngx-tw-time-picker.d.ts +303 -0
  168. package/types/cdevhub-ngx-tw-timeline.d.ts +278 -0
  169. package/types/cdevhub-ngx-tw-toast.d.ts +388 -0
  170. package/types/cdevhub-ngx-tw-tooltip.d.ts +95 -0
  171. package/types/cdevhub-ngx-tw.d.ts +49 -0
@@ -0,0 +1,721 @@
1
+ import * as i0 from '@angular/core';
2
+ import { InjectionToken, input, Directive, output, viewChild, inject, computed, ChangeDetectionStrategy, Component, TemplateRef, model, ViewContainerRef, Injector, DestroyRef, contentChildren, contentChild, signal, linkedSignal, effect, untracked } from '@angular/core';
3
+ import { NgTemplateOutlet } from '@angular/common';
4
+ import { Overlay } from '@angular/cdk/overlay';
5
+ import { ComponentPortal } from '@angular/cdk/portal';
6
+ import { FocusTrapFactory, LiveAnnouncer } from '@angular/cdk/a11y';
7
+ import { Subscription } from 'rxjs';
8
+ import { tv } from 'tailwind-variants';
9
+
10
+ /** Injection token providing a `CommandPaletteRef` to palette overlay content. */
11
+ const COMMAND_PALETTE_REF = new InjectionToken('COMMAND_PALETTE_REF');
12
+
13
+ // ── Constants ──
14
+ /** Duration of leave animation (ms) — matches scale-out/fade-out in theme/_base.css. */
15
+ const ANIMATION_DURATION = 120;
16
+ /** Debounce window (ms) for live-region result announcements while typing. */
17
+ const ANNOUNCE_DEBOUNCE = 200;
18
+ let nextPaletteId = 0;
19
+ // ── Variant config ──
20
+ const commandPaletteVariants = tv({
21
+ slots: {
22
+ panel: 'pointer-events-auto w-full max-w-xl mt-[15vh] bg-surface-overlay text-fg rounded-lg border border-border shadow-md overflow-hidden flex flex-col max-h-[70vh] outline-none',
23
+ searchWrapper: 'flex items-center gap-3 border-b border-border px-4',
24
+ searchInput: 'flex-1 bg-transparent border-0 outline-none text-fg placeholder:text-fg-subtle',
25
+ searchIcon: 'size-5 shrink-0 text-fg-muted',
26
+ list: 'flex-1 overflow-y-auto py-1',
27
+ groupHeader: 'px-4 text-xs font-semibold uppercase tracking-wide text-fg-subtle',
28
+ item: 'relative flex items-center gap-3 px-4 select-none transition-colors duration-normal motion-reduce:transition-none text-fg outline-none',
29
+ itemBody: 'flex-1 min-w-0 flex flex-col',
30
+ itemLabel: 'min-w-0 truncate',
31
+ itemDescription: 'block text-xs text-fg-muted mt-0.5 min-w-0 truncate',
32
+ itemShortcut: 'ml-auto flex items-center gap-1 pl-3 shrink-0',
33
+ itemKbd: 'inline-flex items-center justify-center min-w-5 h-5 px-1 rounded-md border border-border bg-surface-muted text-fg-muted text-2xs font-mono',
34
+ empty: 'px-4 py-10 text-center text-sm text-fg-muted',
35
+ footer: 'border-t border-border px-4 py-2 bg-surface-muted',
36
+ },
37
+ variants: {
38
+ size: {
39
+ xs: {
40
+ item: 'py-1 text-xs',
41
+ searchInput: 'py-2 text-xs',
42
+ groupHeader: 'pt-2 pb-0.5',
43
+ },
44
+ sm: {
45
+ item: 'py-1.5 text-xs',
46
+ searchInput: 'py-2.5 text-xs',
47
+ groupHeader: 'pt-2 pb-0.5',
48
+ },
49
+ md: {
50
+ item: 'py-2 text-sm',
51
+ searchInput: 'py-3 text-sm',
52
+ groupHeader: 'pt-3 pb-1',
53
+ },
54
+ lg: {
55
+ item: 'py-2.5 text-sm',
56
+ searchInput: 'py-3.5 text-base',
57
+ groupHeader: 'pt-3 pb-1',
58
+ },
59
+ xl: {
60
+ item: 'py-3 text-base',
61
+ searchInput: 'py-4 text-base',
62
+ groupHeader: 'pt-4 pb-1',
63
+ },
64
+ },
65
+ // Active option uses `bg-surface-sunken` (one step recessed from `bg-surface-muted`)
66
+ // so the activedescendant-listbox carve-out remains unambiguously distinguishable
67
+ // from the hovered non-active state — DOM focus stays on the combobox input, so
68
+ // this background shift is the only visual signal for the keyboard-active option.
69
+ active: {
70
+ true: { item: 'bg-surface-sunken' },
71
+ false: { item: '' },
72
+ },
73
+ disabled: {
74
+ true: { item: 'opacity-50 pointer-events-none cursor-default' },
75
+ false: { item: 'cursor-pointer hover:bg-surface-muted' },
76
+ },
77
+ },
78
+ compoundVariants: [
79
+ {
80
+ // When an item is BOTH active and enabled, drop the non-active hover token so
81
+ // hovering the keyboard-active option doesn't visually demote it to the
82
+ // hovered-non-active state. Without this, twMerge keeps both background classes
83
+ // and the active sunken fill flips to muted on hover.
84
+ active: true,
85
+ disabled: false,
86
+ class: { item: 'hover:bg-surface-sunken' },
87
+ },
88
+ ],
89
+ defaultVariants: { size: 'md', active: false, disabled: false },
90
+ }, { twMerge: true });
91
+ // ── Helpers ──
92
+ function normalizeShortcut(value) {
93
+ if (value == null)
94
+ return [];
95
+ return Array.isArray(value) ? value : [value];
96
+ }
97
+ function defaultFilter(items, query) {
98
+ const trimmed = query.trim().toLowerCase();
99
+ if (!trimmed)
100
+ return items;
101
+ return items.filter((item) => {
102
+ if (item.label.toLowerCase().includes(trimmed))
103
+ return true;
104
+ if (item.group && item.group.toLowerCase().includes(trimmed))
105
+ return true;
106
+ if (item.keywords) {
107
+ for (const keyword of item.keywords) {
108
+ if (keyword.toLowerCase().includes(trimmed))
109
+ return true;
110
+ }
111
+ }
112
+ return false;
113
+ });
114
+ }
115
+ function findFirstEnabled(items) {
116
+ for (let i = 0; i < items.length; i++) {
117
+ if (!items[i].data.disabled)
118
+ return i;
119
+ }
120
+ return 0;
121
+ }
122
+ function findLastEnabled(items) {
123
+ for (let i = items.length - 1; i >= 0; i--) {
124
+ if (!items[i].data.disabled)
125
+ return i;
126
+ }
127
+ return items.length - 1;
128
+ }
129
+ // ── Group directive ──
130
+ /** Optional explicit grouping wrapper. Items placed inside inherit its label. */
131
+ class CommandPaletteGroupDirective {
132
+ /** Group heading text shown above the items. Required. */
133
+ label = input.required(...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
134
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: CommandPaletteGroupDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
135
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.14", type: CommandPaletteGroupDirective, isStandalone: true, selector: "[twCommandPaletteGroup]", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0 });
136
+ }
137
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: CommandPaletteGroupDirective, decorators: [{
138
+ type: Directive,
139
+ args: [{
140
+ selector: '[twCommandPaletteGroup]',
141
+ }]
142
+ }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }] } });
143
+ // ── Item directive (component with projection) ──
144
+ /** A declarative palette item. Consumers project its rendered content as children. */
145
+ class CommandPaletteItemDirective {
146
+ /** Stable identifier for the item. Used as DOM id and in selection payloads. Required. */
147
+ id = input.required(...(ngDevMode ? [{ debugName: "id" }] : /* istanbul ignore next */ []));
148
+ /** Plain-text label used for filtering and default rendering. Defaults to `''`. */
149
+ label = input('', ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
150
+ /** Additional search keywords that match the query but are not rendered. Defaults to `[]`. */
151
+ keywords = input([], ...(ngDevMode ? [{ debugName: "keywords" }] : /* istanbul ignore next */ []));
152
+ /** Explicit group name. Overrides any enclosing `twCommandPaletteGroup`. Defaults to `undefined`. */
153
+ group = input(undefined, ...(ngDevMode ? [{ debugName: "group" }] : /* istanbul ignore next */ []));
154
+ /** Whether the item is disabled. Disabled items render but cannot be activated. Defaults to `false`. */
155
+ disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
156
+ /** Keyboard shortcut hint. A string renders as one kbd; an array renders each key separately. Defaults to `undefined`. */
157
+ shortcut = input(undefined, ...(ngDevMode ? [{ debugName: "shortcut" }] : /* istanbul ignore next */ []));
158
+ /** Secondary description text rendered under the label. Defaults to `''`. */
159
+ description = input('', ...(ngDevMode ? [{ debugName: "description" }] : /* istanbul ignore next */ []));
160
+ /** Callback run before `activated` emits. Defaults to `undefined`. */
161
+ run = input(undefined, ...(ngDevMode ? [{ debugName: "run" }] : /* istanbul ignore next */ []));
162
+ /** Fires when this specific item is activated (Enter or click). */
163
+ activated = output();
164
+ /** Projected template used by the palette overlay to render this item's content. */
165
+ contentTemplate = viewChild('content', ...(ngDevMode ? [{ debugName: "contentTemplate" }] : /* istanbul ignore next */ []));
166
+ parentGroup = inject(CommandPaletteGroupDirective, {
167
+ optional: true,
168
+ skipSelf: true,
169
+ });
170
+ /** Resolved CommandPaletteItem data derived from inputs + parent group. */
171
+ data = computed(() => ({
172
+ id: this.id(),
173
+ label: this.label(),
174
+ keywords: this.keywords(),
175
+ group: this.group() ?? this.parentGroup?.label() ?? undefined,
176
+ disabled: this.disabled(),
177
+ shortcut: this.shortcut(),
178
+ description: this.description() || undefined,
179
+ run: this.run(),
180
+ }), ...(ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []));
181
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: CommandPaletteItemDirective, deps: [], target: i0.ɵɵFactoryTarget.Component });
182
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.2.14", type: CommandPaletteItemDirective, isStandalone: true, selector: "tw-command-palette-item", inputs: { id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: true, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, keywords: { classPropertyName: "keywords", publicName: "keywords", isSignal: true, isRequired: false, transformFunction: null }, group: { classPropertyName: "group", publicName: "group", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, shortcut: { classPropertyName: "shortcut", publicName: "shortcut", isSignal: true, isRequired: false, transformFunction: null }, description: { classPropertyName: "description", publicName: "description", isSignal: true, isRequired: false, transformFunction: null }, run: { classPropertyName: "run", publicName: "run", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { activated: "activated" }, host: { styleAttribute: "display: none" }, viewQueries: [{ propertyName: "contentTemplate", first: true, predicate: ["content"], descendants: true, isSignal: true }], ngImport: i0, template: `<ng-template #content><ng-content /></ng-template>`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
183
+ }
184
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: CommandPaletteItemDirective, decorators: [{
185
+ type: Component,
186
+ args: [{
187
+ selector: 'tw-command-palette-item',
188
+ changeDetection: ChangeDetectionStrategy.OnPush,
189
+ host: {
190
+ style: 'display: none',
191
+ },
192
+ template: `<ng-template #content><ng-content /></ng-template>`,
193
+ }]
194
+ }], propDecorators: { id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: true }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], keywords: [{ type: i0.Input, args: [{ isSignal: true, alias: "keywords", required: false }] }], group: [{ type: i0.Input, args: [{ isSignal: true, alias: "group", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], shortcut: [{ type: i0.Input, args: [{ isSignal: true, alias: "shortcut", required: false }] }], description: [{ type: i0.Input, args: [{ isSignal: true, alias: "description", required: false }] }], run: [{ type: i0.Input, args: [{ isSignal: true, alias: "run", required: false }] }], activated: [{ type: i0.Output, args: ["activated"] }], contentTemplate: [{ type: i0.ViewChild, args: ['content', { isSignal: true }] }] } });
195
+ // ── Item decoration directives ──
196
+ /** Styles a leading icon inside a palette item. */
197
+ class CommandPaletteItemIconDirective {
198
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: CommandPaletteItemIconDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
199
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.14", type: CommandPaletteItemIconDirective, isStandalone: true, selector: "[twCommandPaletteItemIcon]", host: { classAttribute: "size-4 shrink-0 text-fg-muted" }, ngImport: i0 });
200
+ }
201
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: CommandPaletteItemIconDirective, decorators: [{
202
+ type: Directive,
203
+ args: [{
204
+ selector: '[twCommandPaletteItemIcon]',
205
+ host: {
206
+ class: 'size-4 shrink-0 text-fg-muted',
207
+ },
208
+ }]
209
+ }] });
210
+ /** Styles secondary description text inside a palette item. */
211
+ class CommandPaletteItemDescriptionDirective {
212
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: CommandPaletteItemDescriptionDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
213
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.14", type: CommandPaletteItemDescriptionDirective, isStandalone: true, selector: "[twCommandPaletteItemDescription]", host: { classAttribute: "block text-xs text-fg-muted mt-0.5 min-w-0 truncate" }, ngImport: i0 });
214
+ }
215
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: CommandPaletteItemDescriptionDirective, decorators: [{
216
+ type: Directive,
217
+ args: [{
218
+ selector: '[twCommandPaletteItemDescription]',
219
+ host: {
220
+ class: 'block text-xs text-fg-muted mt-0.5 min-w-0 truncate',
221
+ },
222
+ }]
223
+ }] });
224
+ // ── Template directives ──
225
+ /** Structural directive — consumer template rendered when no items match the query. */
226
+ class CommandPaletteEmptyDirective {
227
+ template = inject((TemplateRef));
228
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: CommandPaletteEmptyDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
229
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.14", type: CommandPaletteEmptyDirective, isStandalone: true, selector: "[twCommandPaletteEmpty]", ngImport: i0 });
230
+ }
231
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: CommandPaletteEmptyDirective, decorators: [{
232
+ type: Directive,
233
+ args: [{
234
+ selector: '[twCommandPaletteEmpty]',
235
+ }]
236
+ }] });
237
+ /** Structural directive — consumer template rendered as a sticky footer inside the palette. */
238
+ class CommandPaletteFooterDirective {
239
+ template = inject((TemplateRef));
240
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: CommandPaletteFooterDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
241
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.14", type: CommandPaletteFooterDirective, isStandalone: true, selector: "[twCommandPaletteFooter]", ngImport: i0 });
242
+ }
243
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: CommandPaletteFooterDirective, decorators: [{
244
+ type: Directive,
245
+ args: [{
246
+ selector: '[twCommandPaletteFooter]',
247
+ }]
248
+ }] });
249
+ // ── Overlay component (private, not exported) ──
250
+ class CommandPaletteOverlayComponent {
251
+ /** Host palette instance — provided via the content injector on attach. */
252
+ palette = inject(CommandPaletteComponent);
253
+ listId = `tw-command-palette-list-${nextPaletteId++}`;
254
+ searchInputRef = viewChild('searchInput', ...(ngDevMode ? [{ debugName: "searchInputRef" }] : /* istanbul ignore next */ []));
255
+ variantResult = computed(() => commandPaletteVariants({ size: this.palette.size() }), ...(ngDevMode ? [{ debugName: "variantResult" }] : /* istanbul ignore next */ []));
256
+ panelClasses = computed(() => {
257
+ const base = this.variantResult().panel();
258
+ const custom = this.palette.panelClassResolved();
259
+ return custom ? `${base} ${custom}` : base;
260
+ }, ...(ngDevMode ? [{ debugName: "panelClasses" }] : /* istanbul ignore next */ []));
261
+ searchWrapperClasses = computed(() => this.variantResult().searchWrapper(), ...(ngDevMode ? [{ debugName: "searchWrapperClasses" }] : /* istanbul ignore next */ []));
262
+ searchInputClasses = computed(() => this.variantResult().searchInput(), ...(ngDevMode ? [{ debugName: "searchInputClasses" }] : /* istanbul ignore next */ []));
263
+ searchIconClasses = computed(() => this.variantResult().searchIcon(), ...(ngDevMode ? [{ debugName: "searchIconClasses" }] : /* istanbul ignore next */ []));
264
+ listClasses = computed(() => this.variantResult().list(), ...(ngDevMode ? [{ debugName: "listClasses" }] : /* istanbul ignore next */ []));
265
+ groupHeaderClasses = computed(() => this.variantResult().groupHeader(), ...(ngDevMode ? [{ debugName: "groupHeaderClasses" }] : /* istanbul ignore next */ []));
266
+ itemBodyClasses = computed(() => this.variantResult().itemBody(), ...(ngDevMode ? [{ debugName: "itemBodyClasses" }] : /* istanbul ignore next */ []));
267
+ itemLabelClasses = computed(() => this.variantResult().itemLabel(), ...(ngDevMode ? [{ debugName: "itemLabelClasses" }] : /* istanbul ignore next */ []));
268
+ itemDescriptionClasses = computed(() => this.variantResult().itemDescription(), ...(ngDevMode ? [{ debugName: "itemDescriptionClasses" }] : /* istanbul ignore next */ []));
269
+ itemShortcutClasses = computed(() => this.variantResult().itemShortcut(), ...(ngDevMode ? [{ debugName: "itemShortcutClasses" }] : /* istanbul ignore next */ []));
270
+ itemKbdClasses = computed(() => this.variantResult().itemKbd(), ...(ngDevMode ? [{ debugName: "itemKbdClasses" }] : /* istanbul ignore next */ []));
271
+ emptyClasses = computed(() => this.variantResult().empty(), ...(ngDevMode ? [{ debugName: "emptyClasses" }] : /* istanbul ignore next */ []));
272
+ footerClasses = computed(() => this.variantResult().footer(), ...(ngDevMode ? [{ debugName: "footerClasses" }] : /* istanbul ignore next */ []));
273
+ itemClasses(active, disabled) {
274
+ return commandPaletteVariants({
275
+ size: this.palette.size(),
276
+ active,
277
+ disabled,
278
+ }).item();
279
+ }
280
+ onSearchInput(event) {
281
+ const value = event.target.value;
282
+ this.palette.query.set(value);
283
+ }
284
+ onItemClick(item) {
285
+ this.palette.selectItem(item);
286
+ }
287
+ onItemHover(item) {
288
+ this.palette.setActiveItem(item);
289
+ }
290
+ /** Focuses the search input (called by the palette after attach). */
291
+ focusSearch() {
292
+ this.searchInputRef()?.nativeElement.focus();
293
+ }
294
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: CommandPaletteOverlayComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
295
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: CommandPaletteOverlayComponent, isStandalone: true, selector: "tw-command-palette-overlay", host: { properties: { "attr.data-command-palette-overlay": "\"\"", "animate.enter": "\"scale-in\"", "animate.leave": "\"scale-out\"" }, classAttribute: "fixed inset-0 flex items-start justify-center p-4 pointer-events-none origin-top" }, viewQueries: [{ propertyName: "searchInputRef", first: true, predicate: ["searchInput"], descendants: true, isSignal: true }], ngImport: i0, template: "<div [class]=\"panelClasses()\" role=\"dialog\" [attr.aria-modal]=\"'true'\" [attr.aria-label]=\"palette.ariaLabel()\">\n <div [class]=\"searchWrapperClasses()\">\n <svg [class]=\"searchIconClasses()\" viewBox=\"0 0 20 20\" fill=\"currentColor\" aria-hidden=\"true\">\n <path fill-rule=\"evenodd\" d=\"M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z\" clip-rule=\"evenodd\" />\n </svg>\n <input\n #searchInput\n type=\"text\"\n role=\"combobox\"\n [class]=\"searchInputClasses()\"\n [placeholder]=\"palette.placeholder()\"\n [value]=\"palette.query()\"\n (input)=\"onSearchInput($event)\"\n [attr.aria-label]=\"palette.searchAriaLabel()\"\n [attr.aria-expanded]=\"'true'\"\n [attr.aria-controls]=\"listId\"\n [attr.aria-activedescendant]=\"palette.activeItemId()\"\n autocomplete=\"off\"\n spellcheck=\"false\"\n />\n </div>\n <div [class]=\"listClasses()\" role=\"listbox\" [id]=\"listId\">\n @if (palette.grouped().length === 0) {\n @if (palette.emptyTemplate(); as emptyTpl) {\n <div [class]=\"emptyClasses()\">\n <ng-container [ngTemplateOutlet]=\"emptyTpl\" [ngTemplateOutletContext]=\"{ $implicit: palette.query() }\" />\n </div>\n } @else {\n <div [class]=\"emptyClasses()\">No results found</div>\n }\n } @else {\n @for (group of palette.grouped(); track group.id) {\n <div role=\"group\" [attr.aria-label]=\"group.label ?? null\">\n @if (group.label) {\n <div [class]=\"groupHeaderClasses()\" role=\"presentation\">{{ group.label }}</div>\n }\n @for (item of group.items; track item.data.id) {\n <div\n [id]=\"item.data.id\"\n role=\"option\"\n [class]=\"itemClasses(item.data.id === palette.activeItemId(), item.data.disabled ?? false)\"\n [attr.aria-selected]=\"item.data.id === palette.activeItemId()\"\n [attr.aria-disabled]=\"(item.data.disabled ?? false) ? 'true' : null\"\n (click)=\"onItemClick(item)\"\n (mousemove)=\"onItemHover(item)\"\n >\n <span [class]=\"itemBodyClasses()\">\n @if (item.template; as tpl) {\n <span [class]=\"itemLabelClasses()\">\n <ng-container [ngTemplateOutlet]=\"tpl\" />\n </span>\n } @else {\n <span [class]=\"itemLabelClasses()\">{{ item.data.label }}</span>\n }\n @if (item.data.description) {\n <span [class]=\"itemDescriptionClasses()\">{{ item.data.description }}</span>\n }\n </span>\n @if (item.shortcutKeys.length > 0) {\n <span [class]=\"itemShortcutClasses()\" aria-hidden=\"true\">\n @for (key of item.shortcutKeys; track $index) {\n <kbd [class]=\"itemKbdClasses()\">{{ key }}</kbd>\n }\n </span>\n }\n </div>\n }\n </div>\n }\n }\n </div>\n @if (palette.footerTemplate(); as footerTpl) {\n <div [class]=\"footerClasses()\">\n <ng-container [ngTemplateOutlet]=\"footerTpl\" />\n </div>\n }\n</div>\n", dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
296
+ }
297
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: CommandPaletteOverlayComponent, decorators: [{
298
+ type: Component,
299
+ args: [{ selector: 'tw-command-palette-overlay', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgTemplateOutlet], host: {
300
+ class: 'fixed inset-0 flex items-start justify-center p-4 pointer-events-none origin-top',
301
+ '[attr.data-command-palette-overlay]': '""',
302
+ '[animate.enter]': '"scale-in"',
303
+ '[animate.leave]': '"scale-out"',
304
+ }, template: "<div [class]=\"panelClasses()\" role=\"dialog\" [attr.aria-modal]=\"'true'\" [attr.aria-label]=\"palette.ariaLabel()\">\n <div [class]=\"searchWrapperClasses()\">\n <svg [class]=\"searchIconClasses()\" viewBox=\"0 0 20 20\" fill=\"currentColor\" aria-hidden=\"true\">\n <path fill-rule=\"evenodd\" d=\"M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z\" clip-rule=\"evenodd\" />\n </svg>\n <input\n #searchInput\n type=\"text\"\n role=\"combobox\"\n [class]=\"searchInputClasses()\"\n [placeholder]=\"palette.placeholder()\"\n [value]=\"palette.query()\"\n (input)=\"onSearchInput($event)\"\n [attr.aria-label]=\"palette.searchAriaLabel()\"\n [attr.aria-expanded]=\"'true'\"\n [attr.aria-controls]=\"listId\"\n [attr.aria-activedescendant]=\"palette.activeItemId()\"\n autocomplete=\"off\"\n spellcheck=\"false\"\n />\n </div>\n <div [class]=\"listClasses()\" role=\"listbox\" [id]=\"listId\">\n @if (palette.grouped().length === 0) {\n @if (palette.emptyTemplate(); as emptyTpl) {\n <div [class]=\"emptyClasses()\">\n <ng-container [ngTemplateOutlet]=\"emptyTpl\" [ngTemplateOutletContext]=\"{ $implicit: palette.query() }\" />\n </div>\n } @else {\n <div [class]=\"emptyClasses()\">No results found</div>\n }\n } @else {\n @for (group of palette.grouped(); track group.id) {\n <div role=\"group\" [attr.aria-label]=\"group.label ?? null\">\n @if (group.label) {\n <div [class]=\"groupHeaderClasses()\" role=\"presentation\">{{ group.label }}</div>\n }\n @for (item of group.items; track item.data.id) {\n <div\n [id]=\"item.data.id\"\n role=\"option\"\n [class]=\"itemClasses(item.data.id === palette.activeItemId(), item.data.disabled ?? false)\"\n [attr.aria-selected]=\"item.data.id === palette.activeItemId()\"\n [attr.aria-disabled]=\"(item.data.disabled ?? false) ? 'true' : null\"\n (click)=\"onItemClick(item)\"\n (mousemove)=\"onItemHover(item)\"\n >\n <span [class]=\"itemBodyClasses()\">\n @if (item.template; as tpl) {\n <span [class]=\"itemLabelClasses()\">\n <ng-container [ngTemplateOutlet]=\"tpl\" />\n </span>\n } @else {\n <span [class]=\"itemLabelClasses()\">{{ item.data.label }}</span>\n }\n @if (item.data.description) {\n <span [class]=\"itemDescriptionClasses()\">{{ item.data.description }}</span>\n }\n </span>\n @if (item.shortcutKeys.length > 0) {\n <span [class]=\"itemShortcutClasses()\" aria-hidden=\"true\">\n @for (key of item.shortcutKeys; track $index) {\n <kbd [class]=\"itemKbdClasses()\">{{ key }}</kbd>\n }\n </span>\n }\n </div>\n }\n </div>\n }\n }\n </div>\n @if (palette.footerTemplate(); as footerTpl) {\n <div [class]=\"footerClasses()\">\n <ng-container [ngTemplateOutlet]=\"footerTpl\" />\n </div>\n }\n</div>\n" }]
305
+ }], propDecorators: { searchInputRef: [{ type: i0.ViewChild, args: ['searchInput', { isSignal: true }] }] } });
306
+ // ── Public palette component ──
307
+ class CommandPaletteComponent {
308
+ /** Controls item density and padding across the palette. Defaults to `'md'`. */
309
+ size = input('md', ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
310
+ /** Placeholder text shown inside the search input. Defaults to `'Type a command or search…'`. */
311
+ placeholder = input('Type a command or search…', ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
312
+ /** Two-way bindable search query. Reads or resets the current filter. Defaults to `''`. */
313
+ query = model('', ...(ngDevMode ? [{ debugName: "query" }] : /* istanbul ignore next */ []));
314
+ /** Two-way bindable open state. Setting to `true` opens the palette; closing updates it to `false`. Defaults to `false`. */
315
+ open = model(false, ...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
316
+ /** Data-driven command list. Merged with projected items and filtered by `query`. Defaults to `[]`. */
317
+ commands = input([], ...(ngDevMode ? [{ debugName: "commands" }] : /* istanbul ignore next */ []));
318
+ /** Custom filter function. Receives the merged item list and current query, returns the filtered result. Defaults to `undefined` (case-insensitive substring match). */
319
+ filterFn = input(undefined, ...(ngDevMode ? [{ debugName: "filterFn" }] : /* istanbul ignore next */ []));
320
+ /** Whether the palette closes automatically after an item is activated. Defaults to `true` — a command palette is a fire-and-dismiss surface; the special case is a "run many" launcher that opts out. */
321
+ closeOnSelect = input(true, ...(ngDevMode ? [{ debugName: "closeOnSelect" }] : /* istanbul ignore next */ []));
322
+ /** Whether Escape closes the palette. Defaults to `true` — Escape is the universal dismiss key for modal surfaces; the special case is a non-dismissible palette. */
323
+ closeOnEscape = input(true, ...(ngDevMode ? [{ debugName: "closeOnEscape" }] : /* istanbul ignore next */ []));
324
+ /** Whether clicking the backdrop closes the palette. Defaults to `true` — clicking outside a modal is the expected dismiss gesture; the special case is enforcing an explicit choice. */
325
+ closeOnBackdropClick = input(true, ...(ngDevMode ? [{ debugName: "closeOnBackdropClick" }] : /* istanbul ignore next */ []));
326
+ /** Whether the search input is auto-focused when the palette opens. Defaults to `true` — without auto-focus the user must click into the input before typing, defeating the keyboard-first design. */
327
+ autoFocus = input(true, ...(ngDevMode ? [{ debugName: "autoFocus" }] : /* istanbul ignore next */ []));
328
+ /** Accessible label used for the dialog role. Defaults to `'Command palette'`. */
329
+ ariaLabel = input('Command palette', ...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
330
+ /** Accessible label applied to the search input (`role="combobox"`). Defaults to `'Search commands'`. */
331
+ searchAriaLabel = input('Search commands', ...(ngDevMode ? [{ debugName: "searchAriaLabel" }] : /* istanbul ignore next */ []));
332
+ /** Additional classes appended to the overlay panel for consumer customization. Defaults to `''`. */
333
+ panelClass = input('', ...(ngDevMode ? [{ debugName: "panelClass" }] : /* istanbul ignore next */ []));
334
+ /** Fires when a command is activated (via click or Enter). Payload is the resolved `CommandPaletteItem`. */
335
+ itemSelected = output();
336
+ /** Fires after the palette becomes fully visible. */
337
+ opened = output();
338
+ /** Fires after the palette is fully removed from the DOM. */
339
+ closed = output();
340
+ overlay = inject(Overlay);
341
+ viewContainerRef = inject(ViewContainerRef);
342
+ injector = inject(Injector);
343
+ destroyRef = inject(DestroyRef);
344
+ focusTrapFactory = inject(FocusTrapFactory);
345
+ announcer = inject(LiveAnnouncer);
346
+ declarativeItems = contentChildren(CommandPaletteItemDirective, { ...(ngDevMode ? { debugName: "declarativeItems" } : /* istanbul ignore next */ {}), descendants: true });
347
+ emptyDirective = contentChild(CommandPaletteEmptyDirective, ...(ngDevMode ? [{ debugName: "emptyDirective" }] : /* istanbul ignore next */ []));
348
+ footerDirective = contentChild(CommandPaletteFooterDirective, ...(ngDevMode ? [{ debugName: "footerDirective" }] : /* istanbul ignore next */ []));
349
+ overlayRef = null;
350
+ overlayInstance = null;
351
+ focusTrap = null;
352
+ perOpenSubs = null;
353
+ previousFocus = null;
354
+ closing = false;
355
+ closeTimer = null;
356
+ announceTimer = null;
357
+ isAttached = signal(false, ...(ngDevMode ? [{ debugName: "isAttached" }] : /* istanbul ignore next */ []));
358
+ /** Merged (declarative + data-array) items keyed by id — declarative wins on collision. */
359
+ allItems = computed(() => {
360
+ const declarative = this.declarativeItems();
361
+ const declarativeResolved = declarative.map((dir) => ({
362
+ data: dir.data(),
363
+ directive: dir,
364
+ template: dir.contentTemplate(),
365
+ shortcutKeys: normalizeShortcut(dir.shortcut()),
366
+ }));
367
+ const seenIds = new Set(declarativeResolved.map((it) => it.data.id));
368
+ const arrayItems = this.commands();
369
+ const arrayResolved = [];
370
+ for (const data of arrayItems) {
371
+ if (seenIds.has(data.id))
372
+ continue;
373
+ seenIds.add(data.id);
374
+ arrayResolved.push({
375
+ data,
376
+ shortcutKeys: normalizeShortcut(data.shortcut),
377
+ });
378
+ }
379
+ return [...declarativeResolved, ...arrayResolved];
380
+ }, ...(ngDevMode ? [{ debugName: "allItems" }] : /* istanbul ignore next */ []));
381
+ /** Items after applying the filter function. */
382
+ filteredItems = computed(() => {
383
+ const all = this.allItems();
384
+ const q = this.query();
385
+ const dataList = all.map((r) => r.data);
386
+ const fn = this.filterFn();
387
+ const filtered = fn ? fn(dataList, q) : defaultFilter(dataList, q);
388
+ const byId = new Map();
389
+ for (const r of all)
390
+ byId.set(r.data.id, r);
391
+ const result = [];
392
+ for (const data of filtered) {
393
+ const resolved = byId.get(data.id);
394
+ if (resolved)
395
+ result.push(resolved);
396
+ }
397
+ return result;
398
+ }, ...(ngDevMode ? [{ debugName: "filteredItems" }] : /* istanbul ignore next */ []));
399
+ /** Filtered items grouped by their `group` field. */
400
+ grouped = computed(() => {
401
+ const items = this.filteredItems();
402
+ const groups = [];
403
+ const byKey = new Map();
404
+ for (const item of items) {
405
+ const label = item.data.group;
406
+ const key = label ?? '__ungrouped__';
407
+ let g = byKey.get(key);
408
+ if (!g) {
409
+ g = { id: `group-${key}`, label, items: [] };
410
+ byKey.set(key, g);
411
+ groups.push(g);
412
+ }
413
+ g.items.push(item);
414
+ }
415
+ return groups;
416
+ }, ...(ngDevMode ? [{ debugName: "grouped" }] : /* istanbul ignore next */ []));
417
+ /**
418
+ * Active item index, auto-resets to the first enabled item when the filtered
419
+ * *id set* changes. Keyed off the id sequence (not the array reference) so a
420
+ * re-emission of `filteredItems` with identical ids preserves the user's
421
+ * keyboard selection; only when ids actually shift do we recompute. When the
422
+ * previously-active id is still present in the new sequence its index is
423
+ * carried over; otherwise we fall back to the first enabled item.
424
+ */
425
+ activeIndex = linkedSignal({ ...(ngDevMode ? { debugName: "activeIndex" } : /* istanbul ignore next */ {}), source: () => this.filteredItems().map((i) => i.data.id),
426
+ computation: (ids, previous) => {
427
+ const items = this.filteredItems();
428
+ if (!previous)
429
+ return findFirstEnabled(items);
430
+ const prevId = previous.source[previous.value];
431
+ if (prevId) {
432
+ const carriedIndex = ids.indexOf(prevId);
433
+ if (carriedIndex >= 0)
434
+ return carriedIndex;
435
+ }
436
+ return findFirstEnabled(items);
437
+ } });
438
+ /** Id of the currently active item (for `aria-activedescendant`). */
439
+ activeItemId = computed(() => {
440
+ const idx = this.activeIndex();
441
+ const items = this.filteredItems();
442
+ return items[idx]?.data.id ?? null;
443
+ }, ...(ngDevMode ? [{ debugName: "activeItemId" }] : /* istanbul ignore next */ []));
444
+ /** Template-ref of the projected empty state (if any). */
445
+ emptyTemplate = computed(() => this.emptyDirective()?.template ?? null, ...(ngDevMode ? [{ debugName: "emptyTemplate" }] : /* istanbul ignore next */ []));
446
+ /** Template-ref of the projected footer (if any). */
447
+ footerTemplate = computed(() => this.footerDirective()?.template ?? null, ...(ngDevMode ? [{ debugName: "footerTemplate" }] : /* istanbul ignore next */ []));
448
+ /** Resolved panel-class string (array merged into a single string). */
449
+ panelClassResolved = computed(() => {
450
+ const raw = this.panelClass();
451
+ return Array.isArray(raw) ? raw.join(' ') : raw;
452
+ }, ...(ngDevMode ? [{ debugName: "panelClassResolved" }] : /* istanbul ignore next */ []));
453
+ constructor() {
454
+ // React to model changes from the consumer
455
+ effect(() => {
456
+ const shouldOpen = this.open();
457
+ if (shouldOpen && !this.overlayInstance && !this.closing) {
458
+ this.openPalette();
459
+ }
460
+ else if (!shouldOpen && this.overlayInstance && !this.closing) {
461
+ this.closePalette();
462
+ }
463
+ });
464
+ // Debounced live-region announcements while the palette is open
465
+ effect(() => {
466
+ const q = this.query();
467
+ const count = this.filteredItems().length;
468
+ untracked(() => {
469
+ if (!this.isAttached())
470
+ return;
471
+ this.clearAnnounceTimer();
472
+ this.announceTimer = setTimeout(() => {
473
+ const message = count === 0
474
+ ? `No commands match ${q}`
475
+ : `${count} ${count === 1 ? 'result' : 'results'} for ${q}`;
476
+ this.announcer.announce(message, 'polite');
477
+ }, ANNOUNCE_DEBOUNCE);
478
+ });
479
+ });
480
+ this.destroyRef.onDestroy(() => {
481
+ this.clearCloseTimer();
482
+ this.clearAnnounceTimer();
483
+ this.destroyFocusTrap();
484
+ this.perOpenSubs?.unsubscribe();
485
+ this.disposeOverlay();
486
+ });
487
+ }
488
+ // ── Public API ──
489
+ /** Open the palette programmatically. */
490
+ show() {
491
+ if (this.overlayInstance || this.closing)
492
+ return;
493
+ this.open.set(true);
494
+ }
495
+ /** Close the palette programmatically. */
496
+ hide() {
497
+ if (!this.overlayInstance || this.closing)
498
+ return;
499
+ this.closePalette();
500
+ }
501
+ /** Toggle the current open state. */
502
+ toggle() {
503
+ if (this.overlayInstance || this.closing) {
504
+ this.hide();
505
+ }
506
+ else {
507
+ this.show();
508
+ }
509
+ }
510
+ /** Force the palette to reapply focus to the search input. */
511
+ focusSearch() {
512
+ this.overlayInstance?.focusSearch();
513
+ }
514
+ // ── Internal wiring (used by overlay) ──
515
+ /** Activate an item: run callbacks, emit events, optionally close. */
516
+ selectItem(item) {
517
+ if (item.data.disabled)
518
+ return;
519
+ item.data.run?.();
520
+ item.directive?.activated.emit();
521
+ this.itemSelected.emit(item.data);
522
+ if (this.closeOnSelect())
523
+ this.hide();
524
+ }
525
+ /** Move the active descendant to the given item (used on hover). */
526
+ setActiveItem(item) {
527
+ if (item.data.disabled)
528
+ return;
529
+ const idx = this.filteredItems().indexOf(item);
530
+ if (idx >= 0)
531
+ this.activeIndex.set(idx);
532
+ }
533
+ // ── Private ──
534
+ openPalette() {
535
+ this.previousFocus =
536
+ document.activeElement ?? null;
537
+ this.ensureOverlay();
538
+ this.attachContent();
539
+ this.subscribePerOpen();
540
+ this.isAttached.set(true);
541
+ this.setupFocusTrap();
542
+ if (this.autoFocus()) {
543
+ queueMicrotask(() => this.overlayInstance?.focusSearch());
544
+ }
545
+ this.announcer.announce(`Command palette opened. ${this.filteredItems().length} commands available.`, 'polite');
546
+ this.opened.emit();
547
+ }
548
+ closePalette() {
549
+ if (this.closing)
550
+ return;
551
+ this.closing = true;
552
+ this.destroyFocusTrap();
553
+ this.previousFocus?.focus();
554
+ this.previousFocus = null;
555
+ this.closeTimer = setTimeout(() => {
556
+ this.closeTimer = null;
557
+ // Guard against teardown during the animation window. `destroyRef.onDestroy`
558
+ // calls `clearCloseTimer()` so this branch only fires when the timer survived;
559
+ // even so, `isAttached` is the authoritative signal — if the overlay already
560
+ // detached (programmatic disposeOverlay, double-close race), skip writes that
561
+ // would touch a destroyed instance or re-emit `open=false`.
562
+ if (!this.isAttached()) {
563
+ this.closing = false;
564
+ return;
565
+ }
566
+ if (this.overlayRef?.hasAttached()) {
567
+ this.overlayRef.detach();
568
+ }
569
+ this.perOpenSubs?.unsubscribe();
570
+ this.perOpenSubs = null;
571
+ this.overlayInstance = null;
572
+ this.isAttached.set(false);
573
+ untracked(() => this.open.set(false));
574
+ this.closed.emit();
575
+ this.closing = false;
576
+ }, ANIMATION_DURATION);
577
+ }
578
+ ensureOverlay() {
579
+ if (this.overlayRef)
580
+ return;
581
+ const positionStrategy = this.overlay.position().global();
582
+ const scrollStrategy = this.overlay.scrollStrategies.block();
583
+ this.overlayRef = this.overlay.create({
584
+ positionStrategy,
585
+ scrollStrategy,
586
+ hasBackdrop: true,
587
+ backdropClass: 'tw-dialog-backdrop',
588
+ panelClass: 'tw-command-palette-panel',
589
+ });
590
+ }
591
+ attachContent() {
592
+ if (!this.overlayRef)
593
+ return;
594
+ const paletteRef = {
595
+ close: () => this.hide(),
596
+ setQuery: (q) => this.query.set(q),
597
+ };
598
+ const contentInjector = Injector.create({
599
+ parent: this.injector,
600
+ providers: [
601
+ { provide: CommandPaletteComponent, useValue: this },
602
+ { provide: COMMAND_PALETTE_REF, useValue: paletteRef },
603
+ ],
604
+ });
605
+ const portal = new ComponentPortal(CommandPaletteOverlayComponent, this.viewContainerRef, contentInjector);
606
+ const ref = this.overlayRef.attach(portal);
607
+ this.overlayInstance = ref.instance;
608
+ }
609
+ subscribePerOpen() {
610
+ this.perOpenSubs?.unsubscribe();
611
+ this.perOpenSubs = new Subscription();
612
+ this.perOpenSubs.add(this.overlayRef.backdropClick().subscribe(() => {
613
+ if (this.closeOnBackdropClick())
614
+ this.hide();
615
+ }));
616
+ this.perOpenSubs.add(this.overlayRef.keydownEvents().subscribe((event) => this.handleOverlayKeydown(event)));
617
+ }
618
+ handleOverlayKeydown(event) {
619
+ switch (event.key) {
620
+ case 'Escape':
621
+ if (this.closeOnEscape()) {
622
+ event.preventDefault();
623
+ this.hide();
624
+ }
625
+ break;
626
+ case 'ArrowDown':
627
+ event.preventDefault();
628
+ this.moveActive(1);
629
+ break;
630
+ case 'ArrowUp':
631
+ event.preventDefault();
632
+ this.moveActive(-1);
633
+ break;
634
+ case 'Home':
635
+ event.preventDefault();
636
+ this.activeIndex.set(findFirstEnabled(this.filteredItems()));
637
+ break;
638
+ case 'End':
639
+ event.preventDefault();
640
+ this.activeIndex.set(findLastEnabled(this.filteredItems()));
641
+ break;
642
+ case 'Enter':
643
+ event.preventDefault();
644
+ this.activateActive();
645
+ break;
646
+ // Tab is intentionally NOT handled here — the FocusTrap installed in
647
+ // `setupFocusTrap()` cycles focus through the modal's focusable elements,
648
+ // so the user cannot accidentally tab out of the palette. Closing on Tab
649
+ // (the previous behaviour) ran counter to the universal "Tab moves focus
650
+ // within a modal" convention and surprised keyboard users.
651
+ }
652
+ }
653
+ moveActive(delta) {
654
+ const items = this.filteredItems();
655
+ if (items.length === 0)
656
+ return;
657
+ const n = items.length;
658
+ let idx = this.activeIndex();
659
+ for (let i = 0; i < n; i++) {
660
+ idx = (idx + delta + n) % n;
661
+ if (!items[idx].data.disabled) {
662
+ this.activeIndex.set(idx);
663
+ return;
664
+ }
665
+ }
666
+ }
667
+ activateActive() {
668
+ const items = this.filteredItems();
669
+ const item = items[this.activeIndex()];
670
+ if (item && !item.data.disabled)
671
+ this.selectItem(item);
672
+ }
673
+ setupFocusTrap() {
674
+ if (!this.overlayRef)
675
+ return;
676
+ this.focusTrap = this.focusTrapFactory.create(this.overlayRef.overlayElement);
677
+ }
678
+ destroyFocusTrap() {
679
+ this.focusTrap?.destroy();
680
+ this.focusTrap = null;
681
+ }
682
+ disposeOverlay() {
683
+ this.overlayRef?.dispose();
684
+ this.overlayRef = null;
685
+ this.overlayInstance = null;
686
+ }
687
+ clearCloseTimer() {
688
+ if (this.closeTimer !== null) {
689
+ clearTimeout(this.closeTimer);
690
+ this.closeTimer = null;
691
+ }
692
+ }
693
+ clearAnnounceTimer() {
694
+ if (this.announceTimer !== null) {
695
+ clearTimeout(this.announceTimer);
696
+ this.announceTimer = null;
697
+ }
698
+ }
699
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: CommandPaletteComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
700
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.2.14", type: CommandPaletteComponent, isStandalone: true, selector: "tw-command-palette", inputs: { size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, query: { classPropertyName: "query", publicName: "query", isSignal: true, isRequired: false, transformFunction: null }, open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null }, commands: { classPropertyName: "commands", publicName: "commands", isSignal: true, isRequired: false, transformFunction: null }, filterFn: { classPropertyName: "filterFn", publicName: "filterFn", isSignal: true, isRequired: false, transformFunction: null }, closeOnSelect: { classPropertyName: "closeOnSelect", publicName: "closeOnSelect", isSignal: true, isRequired: false, transformFunction: null }, closeOnEscape: { classPropertyName: "closeOnEscape", publicName: "closeOnEscape", isSignal: true, isRequired: false, transformFunction: null }, closeOnBackdropClick: { classPropertyName: "closeOnBackdropClick", publicName: "closeOnBackdropClick", isSignal: true, isRequired: false, transformFunction: null }, autoFocus: { classPropertyName: "autoFocus", publicName: "autoFocus", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, searchAriaLabel: { classPropertyName: "searchAriaLabel", publicName: "searchAriaLabel", isSignal: true, isRequired: false, transformFunction: null }, panelClass: { classPropertyName: "panelClass", publicName: "panelClass", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { query: "queryChange", open: "openChange", itemSelected: "itemSelected", opened: "opened", closed: "closed" }, host: { styleAttribute: "display: none" }, queries: [{ propertyName: "declarativeItems", predicate: CommandPaletteItemDirective, descendants: true, isSignal: true }, { propertyName: "emptyDirective", first: true, predicate: CommandPaletteEmptyDirective, descendants: true, isSignal: true }, { propertyName: "footerDirective", first: true, predicate: CommandPaletteFooterDirective, descendants: true, isSignal: true }], ngImport: i0, template: `<ng-content />`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
701
+ }
702
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: CommandPaletteComponent, decorators: [{
703
+ type: Component,
704
+ args: [{
705
+ selector: 'tw-command-palette',
706
+ changeDetection: ChangeDetectionStrategy.OnPush,
707
+ host: {
708
+ style: 'display: none',
709
+ },
710
+ template: `<ng-content />`,
711
+ }]
712
+ }], ctorParameters: () => [], propDecorators: { size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], query: [{ type: i0.Input, args: [{ isSignal: true, alias: "query", required: false }] }, { type: i0.Output, args: ["queryChange"] }], open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }, { type: i0.Output, args: ["openChange"] }], commands: [{ type: i0.Input, args: [{ isSignal: true, alias: "commands", required: false }] }], filterFn: [{ type: i0.Input, args: [{ isSignal: true, alias: "filterFn", required: false }] }], closeOnSelect: [{ type: i0.Input, args: [{ isSignal: true, alias: "closeOnSelect", required: false }] }], closeOnEscape: [{ type: i0.Input, args: [{ isSignal: true, alias: "closeOnEscape", required: false }] }], closeOnBackdropClick: [{ type: i0.Input, args: [{ isSignal: true, alias: "closeOnBackdropClick", required: false }] }], autoFocus: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoFocus", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], searchAriaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchAriaLabel", required: false }] }], panelClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "panelClass", required: false }] }], itemSelected: [{ type: i0.Output, args: ["itemSelected"] }], opened: [{ type: i0.Output, args: ["opened"] }], closed: [{ type: i0.Output, args: ["closed"] }], declarativeItems: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => CommandPaletteItemDirective), { ...{
713
+ descendants: true,
714
+ }, isSignal: true }] }], emptyDirective: [{ type: i0.ContentChild, args: [i0.forwardRef(() => CommandPaletteEmptyDirective), { isSignal: true }] }], footerDirective: [{ type: i0.ContentChild, args: [i0.forwardRef(() => CommandPaletteFooterDirective), { isSignal: true }] }] } });
715
+
716
+ /**
717
+ * Generated bundle index. Do not edit.
718
+ */
719
+
720
+ export { COMMAND_PALETTE_REF, CommandPaletteComponent, CommandPaletteEmptyDirective, CommandPaletteFooterDirective, CommandPaletteGroupDirective, CommandPaletteItemDescriptionDirective, CommandPaletteItemDirective, CommandPaletteItemIconDirective };
721
+ //# sourceMappingURL=cdevhub-ngx-tw-command-palette.mjs.map