@sonny-ui/core 0.1.0-alpha.2 → 0.1.0-alpha.20

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 (242) hide show
  1. package/README.md +187 -40
  2. package/fesm2022/sonny-ui-core.mjs +6642 -268
  3. package/fesm2022/sonny-ui-core.mjs.map +1 -1
  4. package/package.json +8 -5
  5. package/schematics/ng-add/index.js +27 -0
  6. package/schematics/ng-add/schema.json +1 -1
  7. package/schematics/ng-generate/component/index.js +182 -1
  8. package/schematics/ng-generate/component/schema.json +2 -2
  9. package/src/lib/accordion/accordion.directives.spec.ts +173 -0
  10. package/src/lib/accordion/accordion.directives.ts +143 -0
  11. package/src/lib/accordion/index.ts +8 -0
  12. package/src/lib/alert/alert.directives.spec.ts +154 -0
  13. package/src/lib/alert/alert.directives.ts +67 -0
  14. package/src/lib/alert/alert.variants.ts +25 -0
  15. package/src/lib/alert/index.ts +6 -0
  16. package/src/lib/avatar/avatar.component.spec.ts +75 -0
  17. package/src/lib/avatar/avatar.component.ts +43 -0
  18. package/src/lib/avatar/avatar.variants.ts +26 -0
  19. package/src/lib/avatar/index.ts +2 -0
  20. package/src/lib/avatar-group/avatar-group.component.spec.ts +74 -0
  21. package/src/lib/avatar-group/avatar-group.component.ts +88 -0
  22. package/src/lib/avatar-group/index.ts +1 -0
  23. package/src/lib/badge/badge.directive.spec.ts +74 -0
  24. package/src/lib/badge/badge.directive.ts +17 -0
  25. package/src/lib/badge/badge.variants.ts +29 -0
  26. package/src/lib/badge/index.ts +2 -0
  27. package/src/lib/breadcrumb/breadcrumb.directives.spec.ts +80 -0
  28. package/src/lib/breadcrumb/breadcrumb.directives.ts +78 -0
  29. package/src/lib/breadcrumb/index.ts +8 -0
  30. package/src/lib/button/button.directive.spec.ts +92 -0
  31. package/src/lib/button/button.directive.ts +28 -0
  32. package/src/lib/button/button.variants.ts +30 -0
  33. package/src/lib/button/index.ts +2 -0
  34. package/src/lib/button-group/button-group.directive.spec.ts +46 -0
  35. package/src/lib/button-group/button-group.directive.ts +19 -0
  36. package/src/lib/button-group/button-group.variants.ts +18 -0
  37. package/src/lib/button-group/index.ts +2 -0
  38. package/src/lib/calendar/calendar.component.spec.ts +192 -0
  39. package/src/lib/calendar/calendar.component.ts +342 -0
  40. package/src/lib/calendar/calendar.types.ts +24 -0
  41. package/src/lib/calendar/index.ts +7 -0
  42. package/src/lib/card/card.directives.spec.ts +104 -0
  43. package/src/lib/card/card.directives.ts +72 -0
  44. package/src/lib/card/card.variants.ts +28 -0
  45. package/src/lib/card/index.ts +9 -0
  46. package/src/lib/carousel/carousel.directives.spec.ts +85 -0
  47. package/src/lib/carousel/carousel.directives.ts +159 -0
  48. package/src/lib/carousel/index.ts +8 -0
  49. package/src/lib/chat-bubble/chat-bubble.directives.spec.ts +52 -0
  50. package/src/lib/chat-bubble/chat-bubble.directives.ts +96 -0
  51. package/src/lib/chat-bubble/index.ts +11 -0
  52. package/src/lib/checkbox/checkbox.directive.spec.ts +57 -0
  53. package/src/lib/checkbox/checkbox.directive.ts +16 -0
  54. package/src/lib/checkbox/checkbox.variants.ts +19 -0
  55. package/src/lib/checkbox/index.ts +2 -0
  56. package/src/lib/color-picker/color-picker.component.spec.ts +328 -0
  57. package/src/lib/color-picker/color-picker.component.ts +537 -0
  58. package/src/lib/color-picker/color-picker.types.ts +24 -0
  59. package/src/lib/color-picker/color-picker.utils.ts +183 -0
  60. package/src/lib/color-picker/color-picker.variants.ts +17 -0
  61. package/src/lib/color-picker/index.ts +20 -0
  62. package/src/lib/combobox/combobox.component.spec.ts +151 -0
  63. package/src/lib/combobox/combobox.component.ts +264 -0
  64. package/src/lib/combobox/combobox.variants.ts +19 -0
  65. package/src/lib/combobox/index.ts +2 -0
  66. package/src/lib/command-palette/command-palette.component.spec.ts +178 -0
  67. package/src/lib/command-palette/command-palette.component.ts +194 -0
  68. package/src/lib/command-palette/command-palette.service.ts +36 -0
  69. package/src/lib/command-palette/command-palette.types.ts +23 -0
  70. package/src/lib/command-palette/index.ts +7 -0
  71. package/src/lib/data-table/data-table.component.spec.ts +443 -0
  72. package/src/lib/data-table/data-table.component.ts +602 -0
  73. package/src/lib/data-table/data-table.directives.ts +31 -0
  74. package/src/lib/data-table/data-table.types.ts +20 -0
  75. package/src/lib/data-table/index.ts +13 -0
  76. package/src/lib/date-picker/date-picker.component.spec.ts +131 -0
  77. package/src/lib/date-picker/date-picker.component.ts +220 -0
  78. package/src/lib/date-picker/date-picker.variants.ts +17 -0
  79. package/src/lib/date-picker/index.ts +2 -0
  80. package/src/lib/date-range-picker/date-range-picker.component.spec.ts +151 -0
  81. package/src/lib/date-range-picker/date-range-picker.component.ts +340 -0
  82. package/src/lib/date-range-picker/index.ts +1 -0
  83. package/src/lib/diff/diff.component.spec.ts +47 -0
  84. package/src/lib/diff/diff.component.ts +82 -0
  85. package/src/lib/diff/index.ts +1 -0
  86. package/src/lib/divider/divider.component.spec.ts +48 -0
  87. package/src/lib/divider/divider.component.ts +51 -0
  88. package/src/lib/divider/divider.variants.ts +22 -0
  89. package/src/lib/divider/index.ts +2 -0
  90. package/src/lib/dock/dock.directives.spec.ts +85 -0
  91. package/src/lib/dock/dock.directives.ts +81 -0
  92. package/src/lib/dock/index.ts +1 -0
  93. package/src/lib/drawer/drawer.directives.spec.ts +62 -0
  94. package/src/lib/drawer/drawer.directives.ts +80 -0
  95. package/src/lib/drawer/index.ts +8 -0
  96. package/src/lib/dropdown/dropdown.directives.spec.ts +106 -0
  97. package/src/lib/dropdown/dropdown.directives.ts +136 -0
  98. package/src/lib/dropdown/dropdown.variants.ts +27 -0
  99. package/src/lib/dropdown/index.ts +15 -0
  100. package/src/lib/fab/fab.directives.spec.ts +60 -0
  101. package/src/lib/fab/fab.directives.ts +77 -0
  102. package/src/lib/fab/index.ts +8 -0
  103. package/src/lib/fieldset/fieldset.directives.spec.ts +74 -0
  104. package/src/lib/fieldset/fieldset.directives.ts +49 -0
  105. package/src/lib/fieldset/fieldset.variants.ts +15 -0
  106. package/src/lib/fieldset/index.ts +6 -0
  107. package/src/lib/file-input/file-input.component.spec.ts +114 -0
  108. package/src/lib/file-input/file-input.component.ts +155 -0
  109. package/src/lib/file-input/file-input.variants.ts +25 -0
  110. package/src/lib/file-input/index.ts +6 -0
  111. package/src/lib/indicator/index.ts +6 -0
  112. package/src/lib/indicator/indicator.directives.spec.ts +64 -0
  113. package/src/lib/indicator/indicator.directives.ts +59 -0
  114. package/src/lib/input/index.ts +3 -0
  115. package/src/lib/input/input.directive.spec.ts +103 -0
  116. package/src/lib/input/input.directive.ts +25 -0
  117. package/src/lib/input/input.variants.ts +42 -0
  118. package/src/lib/input/label.directive.ts +16 -0
  119. package/src/lib/kbd/index.ts +2 -0
  120. package/src/lib/kbd/kbd.directive.spec.ts +42 -0
  121. package/src/lib/kbd/kbd.directive.ts +18 -0
  122. package/src/lib/kbd/kbd.variants.ts +19 -0
  123. package/src/lib/link/index.ts +2 -0
  124. package/src/lib/link/link.directive.spec.ts +41 -0
  125. package/src/lib/link/link.directive.ts +18 -0
  126. package/src/lib/link/link.variants.ts +20 -0
  127. package/src/lib/list/index.ts +8 -0
  128. package/src/lib/list/list.directives.spec.ts +65 -0
  129. package/src/lib/list/list.directives.ts +81 -0
  130. package/src/lib/loader/index.ts +2 -0
  131. package/src/lib/loader/loader.component.spec.ts +58 -0
  132. package/src/lib/loader/loader.component.ts +47 -0
  133. package/src/lib/loader/loader.variants.ts +21 -0
  134. package/src/lib/modal/dialog-ref.ts +19 -0
  135. package/src/lib/modal/dialog.directives.ts +84 -0
  136. package/src/lib/modal/dialog.service.spec.ts +52 -0
  137. package/src/lib/modal/dialog.service.ts +61 -0
  138. package/src/lib/modal/dialog.types.ts +16 -0
  139. package/src/lib/modal/index.ts +11 -0
  140. package/src/lib/navbar/index.ts +7 -0
  141. package/src/lib/navbar/navbar.directives.spec.ts +59 -0
  142. package/src/lib/navbar/navbar.directives.ts +57 -0
  143. package/src/lib/number-input/index.ts +2 -0
  144. package/src/lib/number-input/number-input.component.spec.ts +151 -0
  145. package/src/lib/number-input/number-input.component.ts +152 -0
  146. package/src/lib/number-input/number-input.variants.ts +17 -0
  147. package/src/lib/otp-input/index.ts +2 -0
  148. package/src/lib/otp-input/otp-input.component.spec.ts +252 -0
  149. package/src/lib/otp-input/otp-input.component.ts +274 -0
  150. package/src/lib/otp-input/otp-input.variants.ts +18 -0
  151. package/src/lib/pagination/index.ts +6 -0
  152. package/src/lib/pagination/pagination.component.spec.ts +59 -0
  153. package/src/lib/pagination/pagination.component.ts +143 -0
  154. package/src/lib/pagination/pagination.variants.ts +31 -0
  155. package/src/lib/popover/index.ts +6 -0
  156. package/src/lib/popover/popover.directives.spec.ts +147 -0
  157. package/src/lib/popover/popover.directives.ts +151 -0
  158. package/src/lib/progress/index.ts +7 -0
  159. package/src/lib/progress/progress.component.spec.ts +117 -0
  160. package/src/lib/progress/progress.component.ts +64 -0
  161. package/src/lib/progress/progress.variants.ts +43 -0
  162. package/src/lib/radial-progress/index.ts +5 -0
  163. package/src/lib/radial-progress/radial-progress.component.spec.ts +41 -0
  164. package/src/lib/radial-progress/radial-progress.component.ts +70 -0
  165. package/src/lib/radio/index.ts +2 -0
  166. package/src/lib/radio/radio.directive.spec.ts +46 -0
  167. package/src/lib/radio/radio.directive.ts +16 -0
  168. package/src/lib/radio/radio.variants.ts +19 -0
  169. package/src/lib/rating/index.ts +2 -0
  170. package/src/lib/rating/rating.component.spec.ts +157 -0
  171. package/src/lib/rating/rating.component.ts +163 -0
  172. package/src/lib/rating/rating.variants.ts +20 -0
  173. package/src/lib/select/index.ts +2 -0
  174. package/src/lib/select/select.component.spec.ts +112 -0
  175. package/src/lib/select/select.component.ts +235 -0
  176. package/src/lib/select/select.variants.ts +19 -0
  177. package/src/lib/sheet/index.ts +10 -0
  178. package/src/lib/sheet/sheet-ref.ts +18 -0
  179. package/src/lib/sheet/sheet.component.spec.ts +67 -0
  180. package/src/lib/sheet/sheet.directives.ts +70 -0
  181. package/src/lib/sheet/sheet.service.ts +100 -0
  182. package/src/lib/sheet/sheet.types.ts +23 -0
  183. package/src/lib/skeleton/index.ts +2 -0
  184. package/src/lib/skeleton/skeleton.directive.spec.ts +63 -0
  185. package/src/lib/skeleton/skeleton.directive.ts +21 -0
  186. package/src/lib/skeleton/skeleton.variants.ts +27 -0
  187. package/src/lib/slider/index.ts +2 -0
  188. package/src/lib/slider/slider.component.spec.ts +104 -0
  189. package/src/lib/slider/slider.component.ts +181 -0
  190. package/src/lib/slider/slider.variants.ts +25 -0
  191. package/src/lib/stat/index.ts +8 -0
  192. package/src/lib/stat/stat.directives.spec.ts +60 -0
  193. package/src/lib/stat/stat.directives.ts +79 -0
  194. package/src/lib/status/index.ts +2 -0
  195. package/src/lib/status/status.directive.spec.ts +43 -0
  196. package/src/lib/status/status.directive.ts +37 -0
  197. package/src/lib/status/status.variants.ts +26 -0
  198. package/src/lib/steps/index.ts +8 -0
  199. package/src/lib/steps/steps.directives.spec.ts +52 -0
  200. package/src/lib/steps/steps.directives.ts +78 -0
  201. package/src/lib/switch/index.ts +2 -0
  202. package/src/lib/switch/switch.component.spec.ts +98 -0
  203. package/src/lib/switch/switch.component.ts +76 -0
  204. package/src/lib/switch/switch.variants.ts +31 -0
  205. package/src/lib/table/index.ts +12 -0
  206. package/src/lib/table/table.directives.spec.ts +111 -0
  207. package/src/lib/table/table.directives.ts +126 -0
  208. package/src/lib/table/table.variants.ts +36 -0
  209. package/src/lib/tabs/index.ts +8 -0
  210. package/src/lib/tabs/tabs.directives.spec.ts +136 -0
  211. package/src/lib/tabs/tabs.directives.ts +126 -0
  212. package/src/lib/tabs/tabs.variants.ts +17 -0
  213. package/src/lib/tag-input/index.ts +2 -0
  214. package/src/lib/tag-input/tag-input.component.spec.ts +190 -0
  215. package/src/lib/tag-input/tag-input.component.ts +172 -0
  216. package/src/lib/tag-input/tag-input.variants.ts +31 -0
  217. package/src/lib/textarea/index.ts +7 -0
  218. package/src/lib/textarea/textarea.directive.spec.ts +84 -0
  219. package/src/lib/textarea/textarea.directive.ts +71 -0
  220. package/src/lib/textarea/textarea.variants.ts +34 -0
  221. package/src/lib/timeline/index.ts +11 -0
  222. package/src/lib/timeline/timeline.directives.spec.ts +55 -0
  223. package/src/lib/timeline/timeline.directives.ts +85 -0
  224. package/src/lib/toast/index.ts +3 -0
  225. package/src/lib/toast/toast.service.spec.ts +71 -0
  226. package/src/lib/toast/toast.service.ts +60 -0
  227. package/src/lib/toast/toast.variants.ts +38 -0
  228. package/src/lib/toast/toaster.component.spec.ts +38 -0
  229. package/src/lib/toast/toaster.component.ts +81 -0
  230. package/src/lib/toggle/index.ts +2 -0
  231. package/src/lib/toggle/toggle.directive.spec.ts +100 -0
  232. package/src/lib/toggle/toggle.directive.ts +61 -0
  233. package/src/lib/toggle/toggle.variants.ts +25 -0
  234. package/src/lib/tooltip/index.ts +2 -0
  235. package/src/lib/tooltip/tooltip.directive.spec.ts +113 -0
  236. package/src/lib/tooltip/tooltip.directive.ts +130 -0
  237. package/src/lib/tooltip/tooltip.variants.ts +20 -0
  238. package/src/lib/validator/index.ts +5 -0
  239. package/src/lib/validator/validator.directives.spec.ts +47 -0
  240. package/src/lib/validator/validator.directives.ts +50 -0
  241. package/src/styles/sonny-theme.css +33 -0
  242. package/types/sonny-ui-core.d.ts +1443 -13
@@ -0,0 +1,178 @@
1
+ import { Component, inject } from '@angular/core';
2
+ import { TestBed, type ComponentFixture } from '@angular/core/testing';
3
+ import { SnyCommandPaletteService } from './command-palette.service';
4
+ import { SnyDialogService } from '../modal/dialog.service';
5
+ import type { Command } from './command-palette.types';
6
+
7
+ function createCommands(): Command[] {
8
+ return [
9
+ { id: 'light', label: 'Set Light Theme', group: 'Theme', action: vi.fn() },
10
+ { id: 'dark', label: 'Set Dark Theme', group: 'Theme', action: vi.fn() },
11
+ { id: 'home', label: 'Go to Home', group: 'Navigation', action: vi.fn(), shortcut: 'Ctrl+H' },
12
+ { id: 'settings', label: 'Open Settings', group: 'Navigation', action: vi.fn(), description: 'Open the settings page' },
13
+ { id: 'copy', label: 'Copy URL', action: vi.fn(), keywords: ['clipboard', 'link'] },
14
+ { id: 'disabled', label: 'Disabled Command', disabled: true, action: vi.fn() },
15
+ ];
16
+ }
17
+
18
+ @Component({
19
+ standalone: true,
20
+ template: `<button (click)="open()">Open</button>`,
21
+ })
22
+ class TestHostComponent {
23
+ readonly service = inject(SnyCommandPaletteService);
24
+ commands = createCommands();
25
+
26
+ open(): void {
27
+ this.service.open({ commands: this.commands });
28
+ }
29
+ }
30
+
31
+ describe('SnyCommandPaletteComponent', () => {
32
+ let fixture: ComponentFixture<TestHostComponent>;
33
+ let host: TestHostComponent;
34
+
35
+ beforeEach(async () => {
36
+ await TestBed.configureTestingModule({
37
+ imports: [TestHostComponent],
38
+ providers: [SnyDialogService, SnyCommandPaletteService],
39
+ }).compileComponents();
40
+ fixture = TestBed.createComponent(TestHostComponent);
41
+ host = fixture.componentInstance;
42
+ fixture.detectChanges();
43
+ });
44
+
45
+ function openPalette(): void {
46
+ host.open();
47
+ fixture.detectChanges();
48
+ }
49
+
50
+ function getPaletteEl(): HTMLElement | null {
51
+ return document.querySelector('sny-command-palette');
52
+ }
53
+
54
+ function getButtons(): HTMLButtonElement[] {
55
+ const el = getPaletteEl();
56
+ return el ? Array.from(el.querySelectorAll('button[data-cmd-idx]')) as HTMLButtonElement[] : [];
57
+ }
58
+
59
+ function getInput(): HTMLInputElement | null {
60
+ const el = getPaletteEl();
61
+ return el ? el.querySelector('input') : null;
62
+ }
63
+
64
+ afterEach(() => {
65
+ host.service.close();
66
+ fixture.detectChanges();
67
+ });
68
+
69
+ it('should open the palette via service', () => {
70
+ openPalette();
71
+ expect(getPaletteEl()).not.toBeNull();
72
+ });
73
+
74
+ it('should prevent double open', () => {
75
+ openPalette();
76
+ host.open();
77
+ fixture.detectChanges();
78
+ const palettes = document.querySelectorAll('sny-command-palette');
79
+ expect(palettes.length).toBe(1);
80
+ });
81
+
82
+ it('should render search input with placeholder', () => {
83
+ openPalette();
84
+ const input = getInput();
85
+ expect(input).not.toBeNull();
86
+ expect(input?.placeholder).toBe('Type a command...');
87
+ });
88
+
89
+ it('should render commands (excluding disabled)', () => {
90
+ openPalette();
91
+ const buttons = getButtons();
92
+ // 6 commands minus 1 disabled = 5
93
+ expect(buttons.length).toBe(5);
94
+ });
95
+
96
+ it('should render group headers', () => {
97
+ openPalette();
98
+ const el = getPaletteEl()!;
99
+ const groups = el.querySelectorAll('.uppercase');
100
+ const groupNames = Array.from(groups).map((g) => g.textContent?.trim());
101
+ expect(groupNames).toContain('Theme');
102
+ expect(groupNames).toContain('Navigation');
103
+ });
104
+
105
+ it('should filter commands by query', () => {
106
+ openPalette();
107
+ const input = getInput()!;
108
+ input.value = 'dark';
109
+ input.dispatchEvent(new Event('input'));
110
+ fixture.detectChanges();
111
+
112
+ const buttons = getButtons();
113
+ expect(buttons.length).toBe(1);
114
+ expect(buttons[0].textContent).toContain('Set Dark Theme');
115
+ });
116
+
117
+ it('should filter by keywords', () => {
118
+ openPalette();
119
+ const input = getInput()!;
120
+ input.value = 'clipboard';
121
+ input.dispatchEvent(new Event('input'));
122
+ fixture.detectChanges();
123
+
124
+ const buttons = getButtons();
125
+ expect(buttons.length).toBe(1);
126
+ expect(buttons[0].textContent).toContain('Copy URL');
127
+ });
128
+
129
+ it('should show empty state when no results', () => {
130
+ openPalette();
131
+ const input = getInput()!;
132
+ input.value = 'xyznonexistent';
133
+ input.dispatchEvent(new Event('input'));
134
+ fixture.detectChanges();
135
+
136
+ const el = getPaletteEl()!;
137
+ expect(el.textContent).toContain('No results found.');
138
+ });
139
+
140
+ it('should render shortcuts', () => {
141
+ openPalette();
142
+ const el = getPaletteEl()!;
143
+ const kbds = el.querySelectorAll('kbd');
144
+ const texts = Array.from(kbds).map((k) => k.textContent?.trim());
145
+ expect(texts).toContain('Ctrl+H');
146
+ });
147
+
148
+ it('should render descriptions', () => {
149
+ openPalette();
150
+ const el = getPaletteEl()!;
151
+ expect(el.textContent).toContain('Open the settings page');
152
+ });
153
+
154
+ it('should execute command on click and close', () => {
155
+ openPalette();
156
+ const buttons = getButtons();
157
+ buttons[0].click();
158
+ fixture.detectChanges();
159
+
160
+ expect(host.commands[0].action).toHaveBeenCalled();
161
+ // Palette should be closed
162
+ setTimeout(() => {
163
+ expect(getPaletteEl()).toBeNull();
164
+ });
165
+ });
166
+
167
+ it('should navigate with arrow keys', () => {
168
+ openPalette();
169
+ const el = getPaletteEl()!;
170
+
171
+ el.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }));
172
+ fixture.detectChanges();
173
+
174
+ const buttons = getButtons();
175
+ // Second button should be active
176
+ expect(buttons[1].className).toContain('bg-accent');
177
+ });
178
+ });
@@ -0,0 +1,194 @@
1
+ import {
2
+ afterNextRender,
3
+ ChangeDetectionStrategy,
4
+ Component,
5
+ computed,
6
+ ElementRef,
7
+ inject,
8
+ signal,
9
+ viewChild,
10
+ } from '@angular/core';
11
+ import { DialogRef } from '@angular/cdk/dialog';
12
+ import { SnyInputDirective } from '../input/input.directive';
13
+ import { SNY_DIALOG_DATA } from '../modal/dialog.service';
14
+ import type { Command, CommandGroup, CommandPaletteConfig } from './command-palette.types';
15
+
16
+ @Component({
17
+ selector: 'sny-command-palette',
18
+ changeDetection: ChangeDetectionStrategy.OnPush,
19
+ imports: [SnyInputDirective],
20
+ host: {
21
+ '(keydown)': 'onKeydown($event)',
22
+ 'class': 'block',
23
+ },
24
+ styles: `
25
+ :host {
26
+ display: block;
27
+ background-color: var(--sny-background);
28
+ border: 1px solid var(--sny-border);
29
+ border-radius: 0.5rem;
30
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
31
+ overflow: hidden;
32
+ }
33
+ `,
34
+ template: `
35
+ <!-- Search -->
36
+ <div class="flex items-center gap-2 border-b border-border px-4 py-3">
37
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-muted-foreground shrink-0"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
38
+ <input
39
+ #searchInput
40
+ snyInput
41
+ class="border-none shadow-none h-8 px-0 focus-visible:ring-0 focus-visible:ring-offset-0"
42
+ [placeholder]="config.placeholder ?? 'Type a command...'"
43
+ [value]="query()"
44
+ (input)="onQueryChange(searchInput.value)"
45
+ />
46
+ <kbd class="rounded border border-border bg-muted px-1.5 py-0.5 font-mono text-[10px] text-muted-foreground shrink-0">Esc</kbd>
47
+ </div>
48
+
49
+ <!-- Results -->
50
+ <div class="max-h-[300px] overflow-y-auto p-2 sny-scrollbar">
51
+ @if (flatResults().length === 0) {
52
+ <p class="py-6 text-center text-sm text-muted-foreground">
53
+ {{ config.emptyText ?? 'No results found.' }}
54
+ </p>
55
+ } @else {
56
+ @for (group of filteredGroups(); track group.name) {
57
+ @if (group.name) {
58
+ <p class="px-2 py-1.5 text-xs font-semibold text-muted-foreground uppercase tracking-wider">{{ group.name }}</p>
59
+ }
60
+ @for (cmd of group.commands; track cmd.id) {
61
+ @let idx = flatIndex(cmd);
62
+ <button
63
+ [attr.data-cmd-idx]="idx"
64
+ [class]="
65
+ 'w-full text-left rounded-sm px-3 py-2 text-sm transition-colors flex items-center gap-3 cursor-pointer ' +
66
+ (idx === activeIndex()
67
+ ? 'bg-accent text-accent-foreground'
68
+ : 'text-foreground hover:bg-accent/50')
69
+ "
70
+ (click)="execute(cmd)"
71
+ (mouseenter)="activeIndex.set(idx)"
72
+ >
73
+ @if (cmd.icon) {
74
+ <span class="shrink-0 w-5 text-center text-muted-foreground" [innerHTML]="cmd.icon"></span>
75
+ }
76
+ <div class="flex-1 min-w-0">
77
+ <span class="block truncate font-medium">{{ cmd.label }}</span>
78
+ @if (cmd.description) {
79
+ <span class="block text-xs text-muted-foreground truncate">{{ cmd.description }}</span>
80
+ }
81
+ </div>
82
+ @if (cmd.shortcut) {
83
+ <kbd class="rounded border border-border bg-muted px-1.5 py-0.5 font-mono text-[10px] text-muted-foreground shrink-0">{{ cmd.shortcut }}</kbd>
84
+ }
85
+ </button>
86
+ }
87
+ }
88
+ }
89
+ </div>
90
+
91
+ <!-- Footer -->
92
+ <div class="border-t border-border px-4 py-2">
93
+ <div class="flex items-center gap-3 text-xs text-muted-foreground">
94
+ <span class="flex items-center gap-1">
95
+ <kbd class="rounded border border-border bg-muted px-1 py-0.5 font-mono text-[10px]">&uarr;&darr;</kbd>
96
+ Navigate
97
+ </span>
98
+ <span class="flex items-center gap-1">
99
+ <kbd class="rounded border border-border bg-muted px-1 py-0.5 font-mono text-[10px]">&crarr;</kbd>
100
+ Execute
101
+ </span>
102
+ <span class="flex items-center gap-1">
103
+ <kbd class="rounded border border-border bg-muted px-1 py-0.5 font-mono text-[10px]">Esc</kbd>
104
+ Close
105
+ </span>
106
+ </div>
107
+ </div>
108
+ `,
109
+ })
110
+ export class SnyCommandPaletteComponent {
111
+ readonly config = inject<CommandPaletteConfig>(SNY_DIALOG_DATA);
112
+ private readonly dialogRef = inject(DialogRef);
113
+ private readonly searchInput = viewChild<ElementRef<HTMLInputElement>>('searchInput');
114
+
115
+ readonly query = signal('');
116
+ readonly activeIndex = signal(0);
117
+
118
+ readonly filteredGroups = computed<CommandGroup[]>(() => {
119
+ const q = this.query().toLowerCase().trim();
120
+ const commands = this.config.commands.filter((cmd) => {
121
+ if (cmd.disabled) return false;
122
+ if (!q) return true;
123
+ return (
124
+ cmd.label.toLowerCase().includes(q) ||
125
+ cmd.description?.toLowerCase().includes(q) ||
126
+ cmd.keywords?.some((k) => k.toLowerCase().includes(q))
127
+ );
128
+ });
129
+
130
+ const groups = new Map<string, Command[]>();
131
+ for (const cmd of commands) {
132
+ const group = cmd.group ?? '';
133
+ if (!groups.has(group)) groups.set(group, []);
134
+ groups.get(group)!.push(cmd);
135
+ }
136
+
137
+ return [...groups.entries()].map(([name, cmds]) => ({ name, commands: cmds }));
138
+ });
139
+
140
+ readonly flatResults = computed(() =>
141
+ this.filteredGroups().flatMap((g) => g.commands)
142
+ );
143
+
144
+ constructor() {
145
+ afterNextRender(() => {
146
+ this.searchInput()?.nativeElement.focus();
147
+ });
148
+ }
149
+
150
+ onQueryChange(value: string): void {
151
+ this.query.set(value);
152
+ this.activeIndex.set(0);
153
+ }
154
+
155
+ flatIndex(cmd: Command): number {
156
+ return this.flatResults().indexOf(cmd);
157
+ }
158
+
159
+ execute(cmd: Command): void {
160
+ this.dialogRef.close();
161
+ cmd.action();
162
+ }
163
+
164
+ onKeydown(event: KeyboardEvent): void {
165
+ const results = this.flatResults();
166
+ const len = results.length;
167
+ if (len === 0 && event.key !== 'Escape') return;
168
+
169
+ switch (event.key) {
170
+ case 'ArrowDown':
171
+ event.preventDefault();
172
+ this.activeIndex.update((i) => (i + 1) % len);
173
+ this.scrollActiveIntoView();
174
+ break;
175
+ case 'ArrowUp':
176
+ event.preventDefault();
177
+ this.activeIndex.update((i) => (i - 1 + len) % len);
178
+ this.scrollActiveIntoView();
179
+ break;
180
+ case 'Enter':
181
+ event.preventDefault();
182
+ const active = results[this.activeIndex()];
183
+ if (active) this.execute(active);
184
+ break;
185
+ }
186
+ }
187
+
188
+ private scrollActiveIntoView(): void {
189
+ requestAnimationFrame(() => {
190
+ const el = document.querySelector(`[data-cmd-idx="${this.activeIndex()}"]`);
191
+ el?.scrollIntoView({ block: 'nearest' });
192
+ });
193
+ }
194
+ }
@@ -0,0 +1,36 @@
1
+ import { Injectable, inject } from '@angular/core';
2
+ import { SnyDialogService } from '../modal/dialog.service';
3
+ import type { SnyDialogRef } from '../modal/dialog-ref';
4
+ import { SnyCommandPaletteComponent } from './command-palette.component';
5
+ import type { CommandPaletteConfig } from './command-palette.types';
6
+
7
+ @Injectable({ providedIn: 'root' })
8
+ export class SnyCommandPaletteService {
9
+ private readonly dialogService = inject(SnyDialogService);
10
+ private isOpen = false;
11
+
12
+ open(config: CommandPaletteConfig): SnyDialogRef<void> | null {
13
+ if (this.isOpen) return null;
14
+ this.isOpen = true;
15
+
16
+ const ref = this.dialogService.open<SnyCommandPaletteComponent, void>(
17
+ SnyCommandPaletteComponent,
18
+ {
19
+ width: config.width ?? '32rem',
20
+ data: config,
21
+ }
22
+ );
23
+
24
+ ref.closed.subscribe(() => {
25
+ this.isOpen = false;
26
+ });
27
+
28
+ return ref;
29
+ }
30
+
31
+ close(): void {
32
+ if (this.isOpen) {
33
+ this.dialogService.closeAll();
34
+ }
35
+ }
36
+ }
@@ -0,0 +1,23 @@
1
+ export interface Command {
2
+ id: string;
3
+ label: string;
4
+ description?: string;
5
+ icon?: string;
6
+ group?: string;
7
+ keywords?: string[];
8
+ shortcut?: string;
9
+ disabled?: boolean;
10
+ action: () => void;
11
+ }
12
+
13
+ export interface CommandGroup {
14
+ name: string;
15
+ commands: Command[];
16
+ }
17
+
18
+ export interface CommandPaletteConfig {
19
+ commands: Command[];
20
+ placeholder?: string;
21
+ emptyText?: string;
22
+ width?: string;
23
+ }
@@ -0,0 +1,7 @@
1
+ export { SnyCommandPaletteComponent } from './command-palette.component';
2
+ export { SnyCommandPaletteService } from './command-palette.service';
3
+ export type {
4
+ Command,
5
+ CommandGroup,
6
+ CommandPaletteConfig,
7
+ } from './command-palette.types';