@tony-ui-library/core 0.0.1

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 (245) hide show
  1. package/README.md +188 -0
  2. package/fesm2022/tony-ui-library-core.mjs +8756 -0
  3. package/fesm2022/tony-ui-library-core.mjs.map +1 -0
  4. package/package.json +55 -0
  5. package/schematics/collection.json +16 -0
  6. package/schematics/ng-add/index.d.ts +5 -0
  7. package/schematics/ng-add/index.js +53 -0
  8. package/schematics/ng-add/schema.json +19 -0
  9. package/schematics/ng-generate/component/index.d.ts +9 -0
  10. package/schematics/ng-generate/component/index.js +439 -0
  11. package/schematics/ng-generate/component/schema.json +32 -0
  12. package/src/lib/accordion/accordion.directives.spec.ts +173 -0
  13. package/src/lib/accordion/accordion.directives.ts +143 -0
  14. package/src/lib/accordion/index.ts +8 -0
  15. package/src/lib/alert/alert.directives.spec.ts +154 -0
  16. package/src/lib/alert/alert.directives.ts +67 -0
  17. package/src/lib/alert/alert.variants.ts +25 -0
  18. package/src/lib/alert/index.ts +6 -0
  19. package/src/lib/avatar/avatar.component.spec.ts +75 -0
  20. package/src/lib/avatar/avatar.component.ts +43 -0
  21. package/src/lib/avatar/avatar.variants.ts +26 -0
  22. package/src/lib/avatar/index.ts +2 -0
  23. package/src/lib/avatar-group/avatar-group.component.spec.ts +74 -0
  24. package/src/lib/avatar-group/avatar-group.component.ts +88 -0
  25. package/src/lib/avatar-group/index.ts +1 -0
  26. package/src/lib/badge/badge.directive.spec.ts +74 -0
  27. package/src/lib/badge/badge.directive.ts +17 -0
  28. package/src/lib/badge/badge.variants.ts +29 -0
  29. package/src/lib/badge/index.ts +2 -0
  30. package/src/lib/breadcrumb/breadcrumb.directives.spec.ts +80 -0
  31. package/src/lib/breadcrumb/breadcrumb.directives.ts +78 -0
  32. package/src/lib/breadcrumb/index.ts +8 -0
  33. package/src/lib/button/button.directive.spec.ts +92 -0
  34. package/src/lib/button/button.directive.ts +28 -0
  35. package/src/lib/button/button.variants.ts +30 -0
  36. package/src/lib/button/index.ts +2 -0
  37. package/src/lib/button-group/button-group.directive.spec.ts +46 -0
  38. package/src/lib/button-group/button-group.directive.ts +19 -0
  39. package/src/lib/button-group/button-group.variants.ts +18 -0
  40. package/src/lib/button-group/index.ts +2 -0
  41. package/src/lib/calendar/calendar.component.spec.ts +192 -0
  42. package/src/lib/calendar/calendar.component.ts +342 -0
  43. package/src/lib/calendar/calendar.types.ts +24 -0
  44. package/src/lib/calendar/index.ts +7 -0
  45. package/src/lib/card/card.directives.spec.ts +104 -0
  46. package/src/lib/card/card.directives.ts +72 -0
  47. package/src/lib/card/card.variants.ts +28 -0
  48. package/src/lib/card/index.ts +9 -0
  49. package/src/lib/carousel/carousel.directives.spec.ts +85 -0
  50. package/src/lib/carousel/carousel.directives.ts +159 -0
  51. package/src/lib/carousel/index.ts +8 -0
  52. package/src/lib/chat-bubble/chat-bubble.directives.spec.ts +52 -0
  53. package/src/lib/chat-bubble/chat-bubble.directives.ts +96 -0
  54. package/src/lib/chat-bubble/index.ts +11 -0
  55. package/src/lib/checkbox/checkbox.directive.spec.ts +57 -0
  56. package/src/lib/checkbox/checkbox.directive.ts +16 -0
  57. package/src/lib/checkbox/checkbox.variants.ts +19 -0
  58. package/src/lib/checkbox/index.ts +2 -0
  59. package/src/lib/color-picker/color-picker.component.spec.ts +328 -0
  60. package/src/lib/color-picker/color-picker.component.ts +537 -0
  61. package/src/lib/color-picker/color-picker.types.ts +24 -0
  62. package/src/lib/color-picker/color-picker.utils.ts +183 -0
  63. package/src/lib/color-picker/color-picker.variants.ts +17 -0
  64. package/src/lib/color-picker/index.ts +20 -0
  65. package/src/lib/combobox/combobox.component.spec.ts +151 -0
  66. package/src/lib/combobox/combobox.component.ts +264 -0
  67. package/src/lib/combobox/combobox.variants.ts +19 -0
  68. package/src/lib/combobox/index.ts +2 -0
  69. package/src/lib/command-palette/command-palette.component.spec.ts +178 -0
  70. package/src/lib/command-palette/command-palette.component.ts +194 -0
  71. package/src/lib/command-palette/command-palette.service.ts +36 -0
  72. package/src/lib/command-palette/command-palette.types.ts +23 -0
  73. package/src/lib/command-palette/index.ts +7 -0
  74. package/src/lib/data-table/data-table.component.spec.ts +443 -0
  75. package/src/lib/data-table/data-table.component.ts +622 -0
  76. package/src/lib/data-table/data-table.directives.ts +31 -0
  77. package/src/lib/data-table/data-table.types.ts +26 -0
  78. package/src/lib/data-table/index.ts +14 -0
  79. package/src/lib/date-picker/date-picker.component.spec.ts +131 -0
  80. package/src/lib/date-picker/date-picker.component.ts +220 -0
  81. package/src/lib/date-picker/date-picker.variants.ts +17 -0
  82. package/src/lib/date-picker/index.ts +2 -0
  83. package/src/lib/date-range-picker/date-range-picker.component.spec.ts +151 -0
  84. package/src/lib/date-range-picker/date-range-picker.component.ts +340 -0
  85. package/src/lib/date-range-picker/index.ts +1 -0
  86. package/src/lib/diff/diff.component.spec.ts +47 -0
  87. package/src/lib/diff/diff.component.ts +82 -0
  88. package/src/lib/diff/index.ts +1 -0
  89. package/src/lib/divider/divider.component.spec.ts +48 -0
  90. package/src/lib/divider/divider.component.ts +51 -0
  91. package/src/lib/divider/divider.variants.ts +22 -0
  92. package/src/lib/divider/index.ts +2 -0
  93. package/src/lib/dock/dock.directives.spec.ts +85 -0
  94. package/src/lib/dock/dock.directives.ts +81 -0
  95. package/src/lib/dock/index.ts +1 -0
  96. package/src/lib/drawer/drawer.directives.spec.ts +62 -0
  97. package/src/lib/drawer/drawer.directives.ts +80 -0
  98. package/src/lib/drawer/index.ts +8 -0
  99. package/src/lib/dropdown/dropdown.directives.spec.ts +106 -0
  100. package/src/lib/dropdown/dropdown.directives.ts +136 -0
  101. package/src/lib/dropdown/dropdown.variants.ts +27 -0
  102. package/src/lib/dropdown/index.ts +15 -0
  103. package/src/lib/fab/fab.directives.spec.ts +60 -0
  104. package/src/lib/fab/fab.directives.ts +77 -0
  105. package/src/lib/fab/index.ts +8 -0
  106. package/src/lib/fieldset/fieldset.directives.spec.ts +74 -0
  107. package/src/lib/fieldset/fieldset.directives.ts +49 -0
  108. package/src/lib/fieldset/fieldset.variants.ts +15 -0
  109. package/src/lib/fieldset/index.ts +6 -0
  110. package/src/lib/file-input/file-input.component.spec.ts +114 -0
  111. package/src/lib/file-input/file-input.component.ts +155 -0
  112. package/src/lib/file-input/file-input.variants.ts +25 -0
  113. package/src/lib/file-input/index.ts +6 -0
  114. package/src/lib/indicator/index.ts +6 -0
  115. package/src/lib/indicator/indicator.directives.spec.ts +64 -0
  116. package/src/lib/indicator/indicator.directives.ts +59 -0
  117. package/src/lib/input/index.ts +3 -0
  118. package/src/lib/input/input.directive.spec.ts +103 -0
  119. package/src/lib/input/input.directive.ts +25 -0
  120. package/src/lib/input/input.variants.ts +42 -0
  121. package/src/lib/input/label.directive.ts +16 -0
  122. package/src/lib/kbd/index.ts +2 -0
  123. package/src/lib/kbd/kbd.directive.spec.ts +42 -0
  124. package/src/lib/kbd/kbd.directive.ts +18 -0
  125. package/src/lib/kbd/kbd.variants.ts +19 -0
  126. package/src/lib/link/index.ts +2 -0
  127. package/src/lib/link/link.directive.spec.ts +41 -0
  128. package/src/lib/link/link.directive.ts +18 -0
  129. package/src/lib/link/link.variants.ts +20 -0
  130. package/src/lib/list/index.ts +8 -0
  131. package/src/lib/list/list.directives.spec.ts +65 -0
  132. package/src/lib/list/list.directives.ts +81 -0
  133. package/src/lib/loader/index.ts +2 -0
  134. package/src/lib/loader/loader.component.spec.ts +58 -0
  135. package/src/lib/loader/loader.component.ts +47 -0
  136. package/src/lib/loader/loader.variants.ts +21 -0
  137. package/src/lib/modal/dialog-ref.ts +19 -0
  138. package/src/lib/modal/dialog.directives.ts +84 -0
  139. package/src/lib/modal/dialog.service.spec.ts +52 -0
  140. package/src/lib/modal/dialog.service.ts +61 -0
  141. package/src/lib/modal/dialog.types.ts +16 -0
  142. package/src/lib/modal/index.ts +11 -0
  143. package/src/lib/navbar/index.ts +7 -0
  144. package/src/lib/navbar/navbar.directives.spec.ts +59 -0
  145. package/src/lib/navbar/navbar.directives.ts +57 -0
  146. package/src/lib/number-input/index.ts +2 -0
  147. package/src/lib/number-input/number-input.component.spec.ts +151 -0
  148. package/src/lib/number-input/number-input.component.ts +152 -0
  149. package/src/lib/number-input/number-input.variants.ts +17 -0
  150. package/src/lib/otp-input/index.ts +2 -0
  151. package/src/lib/otp-input/otp-input.component.spec.ts +252 -0
  152. package/src/lib/otp-input/otp-input.component.ts +274 -0
  153. package/src/lib/otp-input/otp-input.variants.ts +18 -0
  154. package/src/lib/pagination/index.ts +6 -0
  155. package/src/lib/pagination/pagination.component.spec.ts +59 -0
  156. package/src/lib/pagination/pagination.component.ts +143 -0
  157. package/src/lib/pagination/pagination.variants.ts +31 -0
  158. package/src/lib/popover/index.ts +6 -0
  159. package/src/lib/popover/popover.directives.spec.ts +147 -0
  160. package/src/lib/popover/popover.directives.ts +151 -0
  161. package/src/lib/progress/index.ts +7 -0
  162. package/src/lib/progress/progress.component.spec.ts +117 -0
  163. package/src/lib/progress/progress.component.ts +64 -0
  164. package/src/lib/progress/progress.variants.ts +43 -0
  165. package/src/lib/radial-progress/index.ts +5 -0
  166. package/src/lib/radial-progress/radial-progress.component.spec.ts +41 -0
  167. package/src/lib/radial-progress/radial-progress.component.ts +70 -0
  168. package/src/lib/radio/index.ts +2 -0
  169. package/src/lib/radio/radio.directive.spec.ts +46 -0
  170. package/src/lib/radio/radio.directive.ts +16 -0
  171. package/src/lib/radio/radio.variants.ts +19 -0
  172. package/src/lib/rating/index.ts +2 -0
  173. package/src/lib/rating/rating.component.spec.ts +157 -0
  174. package/src/lib/rating/rating.component.ts +163 -0
  175. package/src/lib/rating/rating.variants.ts +20 -0
  176. package/src/lib/select/index.ts +2 -0
  177. package/src/lib/select/select.component.spec.ts +112 -0
  178. package/src/lib/select/select.component.ts +235 -0
  179. package/src/lib/select/select.variants.ts +19 -0
  180. package/src/lib/sheet/index.ts +10 -0
  181. package/src/lib/sheet/sheet-ref.ts +18 -0
  182. package/src/lib/sheet/sheet.component.spec.ts +67 -0
  183. package/src/lib/sheet/sheet.directives.ts +70 -0
  184. package/src/lib/sheet/sheet.service.ts +100 -0
  185. package/src/lib/sheet/sheet.types.ts +23 -0
  186. package/src/lib/skeleton/index.ts +2 -0
  187. package/src/lib/skeleton/skeleton.directive.spec.ts +63 -0
  188. package/src/lib/skeleton/skeleton.directive.ts +21 -0
  189. package/src/lib/skeleton/skeleton.variants.ts +27 -0
  190. package/src/lib/slider/index.ts +2 -0
  191. package/src/lib/slider/slider.component.spec.ts +104 -0
  192. package/src/lib/slider/slider.component.ts +181 -0
  193. package/src/lib/slider/slider.variants.ts +25 -0
  194. package/src/lib/stat/index.ts +8 -0
  195. package/src/lib/stat/stat.directives.spec.ts +60 -0
  196. package/src/lib/stat/stat.directives.ts +79 -0
  197. package/src/lib/status/index.ts +2 -0
  198. package/src/lib/status/status.directive.spec.ts +43 -0
  199. package/src/lib/status/status.directive.ts +37 -0
  200. package/src/lib/status/status.variants.ts +26 -0
  201. package/src/lib/steps/index.ts +8 -0
  202. package/src/lib/steps/steps.directives.spec.ts +52 -0
  203. package/src/lib/steps/steps.directives.ts +78 -0
  204. package/src/lib/switch/index.ts +2 -0
  205. package/src/lib/switch/switch.component.spec.ts +98 -0
  206. package/src/lib/switch/switch.component.ts +76 -0
  207. package/src/lib/switch/switch.variants.ts +31 -0
  208. package/src/lib/table/index.ts +12 -0
  209. package/src/lib/table/table.directives.spec.ts +111 -0
  210. package/src/lib/table/table.directives.ts +126 -0
  211. package/src/lib/table/table.variants.ts +36 -0
  212. package/src/lib/tabs/index.ts +8 -0
  213. package/src/lib/tabs/tabs.directives.spec.ts +136 -0
  214. package/src/lib/tabs/tabs.directives.ts +126 -0
  215. package/src/lib/tabs/tabs.variants.ts +17 -0
  216. package/src/lib/tag-input/index.ts +2 -0
  217. package/src/lib/tag-input/tag-input.component.spec.ts +190 -0
  218. package/src/lib/tag-input/tag-input.component.ts +172 -0
  219. package/src/lib/tag-input/tag-input.variants.ts +31 -0
  220. package/src/lib/textarea/index.ts +7 -0
  221. package/src/lib/textarea/textarea.directive.spec.ts +84 -0
  222. package/src/lib/textarea/textarea.directive.ts +71 -0
  223. package/src/lib/textarea/textarea.variants.ts +34 -0
  224. package/src/lib/timeline/index.ts +11 -0
  225. package/src/lib/timeline/timeline.directives.spec.ts +55 -0
  226. package/src/lib/timeline/timeline.directives.ts +85 -0
  227. package/src/lib/toast/index.ts +3 -0
  228. package/src/lib/toast/toast.service.spec.ts +71 -0
  229. package/src/lib/toast/toast.service.ts +60 -0
  230. package/src/lib/toast/toast.variants.ts +38 -0
  231. package/src/lib/toast/toaster.component.spec.ts +38 -0
  232. package/src/lib/toast/toaster.component.ts +81 -0
  233. package/src/lib/toggle/index.ts +2 -0
  234. package/src/lib/toggle/toggle.directive.spec.ts +100 -0
  235. package/src/lib/toggle/toggle.directive.ts +61 -0
  236. package/src/lib/toggle/toggle.variants.ts +25 -0
  237. package/src/lib/tooltip/index.ts +2 -0
  238. package/src/lib/tooltip/tooltip.directive.spec.ts +113 -0
  239. package/src/lib/tooltip/tooltip.directive.ts +130 -0
  240. package/src/lib/tooltip/tooltip.variants.ts +20 -0
  241. package/src/lib/validator/index.ts +5 -0
  242. package/src/lib/validator/validator.directives.spec.ts +47 -0
  243. package/src/lib/validator/validator.directives.ts +50 -0
  244. package/src/styles/sonny-theme.css +171 -0
  245. package/types/tony-ui-library-core.d.ts +2179 -0
@@ -0,0 +1,112 @@
1
+ import { Component, signal } from '@angular/core';
2
+ import { FormControl, ReactiveFormsModule } from '@angular/forms';
3
+ import { TestBed, ComponentFixture } from '@angular/core/testing';
4
+ import { TonSelectComponent, type SelectOption } from './select.component';
5
+
6
+ @Component({
7
+ standalone: true,
8
+ imports: [TonSelectComponent],
9
+ template: `<ton-select [options]="options" [(value)]="value" [placeholder]="placeholder()" />`,
10
+ })
11
+ class TestHostComponent {
12
+ options: SelectOption[] = [
13
+ { value: 'a', label: 'Option A' },
14
+ { value: 'b', label: 'Option B' },
15
+ { value: 'c', label: 'Option C' },
16
+ ];
17
+ value = signal('');
18
+ placeholder = signal('Pick one...');
19
+ }
20
+
21
+ describe('TonSelectComponent', () => {
22
+ let fixture: ComponentFixture<TestHostComponent>;
23
+ let trigger: HTMLButtonElement;
24
+
25
+ beforeEach(async () => {
26
+ await TestBed.configureTestingModule({
27
+ imports: [TestHostComponent],
28
+ }).compileComponents();
29
+
30
+ fixture = TestBed.createComponent(TestHostComponent);
31
+ fixture.detectChanges();
32
+ trigger = fixture.nativeElement.querySelector('button');
33
+ });
34
+
35
+ it('should show placeholder when no value', () => {
36
+ expect(trigger.textContent).toContain('Pick one...');
37
+ });
38
+
39
+ it('should have combobox role', () => {
40
+ expect(trigger.getAttribute('role')).toBe('combobox');
41
+ expect(trigger.getAttribute('aria-expanded')).toBe('false');
42
+ });
43
+
44
+ it('should open dropdown on click', () => {
45
+ trigger.click();
46
+ fixture.detectChanges();
47
+ expect(trigger.getAttribute('aria-expanded')).toBe('true');
48
+ const options = fixture.nativeElement.querySelectorAll('[role="option"]');
49
+ expect(options.length).toBe(3);
50
+ });
51
+
52
+ it('should show selected label', () => {
53
+ fixture.componentInstance.value.set('b');
54
+ fixture.detectChanges();
55
+ expect(trigger.textContent).toContain('Option B');
56
+ });
57
+ });
58
+
59
+ @Component({
60
+ standalone: true,
61
+ imports: [ReactiveFormsModule, TonSelectComponent],
62
+ template: `<ton-select [options]="options" [formControl]="ctrl" />`,
63
+ })
64
+ class ReactiveFormHost {
65
+ options: SelectOption[] = [
66
+ { value: 'a', label: 'Option A' },
67
+ { value: 'b', label: 'Option B' },
68
+ { value: 'c', label: 'Option C' },
69
+ ];
70
+ ctrl = new FormControl('');
71
+ }
72
+
73
+ describe('TonSelectComponent — Reactive Forms', () => {
74
+ let fixture: ComponentFixture<ReactiveFormHost>;
75
+ let trigger: HTMLButtonElement;
76
+
77
+ beforeEach(async () => {
78
+ await TestBed.configureTestingModule({
79
+ imports: [ReactiveFormHost],
80
+ }).compileComponents();
81
+ fixture = TestBed.createComponent(ReactiveFormHost);
82
+ fixture.detectChanges();
83
+ trigger = fixture.nativeElement.querySelector('button');
84
+ });
85
+
86
+ it('should update view when FormControl value changes (writeValue)', () => {
87
+ fixture.componentInstance.ctrl.setValue('b');
88
+ fixture.detectChanges();
89
+ expect(trigger.textContent).toContain('Option B');
90
+ });
91
+
92
+ it('should update FormControl when user interacts (onChange)', () => {
93
+ trigger.click();
94
+ fixture.detectChanges();
95
+ const option = fixture.nativeElement.querySelector('[role="option"]') as HTMLElement;
96
+ option.dispatchEvent(new Event('mousedown'));
97
+ fixture.detectChanges();
98
+ expect(fixture.componentInstance.ctrl.value).toBe('a');
99
+ });
100
+
101
+ it('should disable via FormControl.disable() (setDisabledState)', () => {
102
+ fixture.componentInstance.ctrl.disable();
103
+ fixture.detectChanges();
104
+ expect(trigger.disabled).toBe(true);
105
+ });
106
+
107
+ it('should mark as touched on blur (onTouched)', () => {
108
+ expect(fixture.componentInstance.ctrl.touched).toBe(false);
109
+ trigger.dispatchEvent(new Event('blur'));
110
+ expect(fixture.componentInstance.ctrl.touched).toBe(true);
111
+ });
112
+ });
@@ -0,0 +1,235 @@
1
+ import {
2
+ ChangeDetectionStrategy,
3
+ Component,
4
+ computed,
5
+ ElementRef,
6
+ forwardRef,
7
+ inject,
8
+ input,
9
+ model,
10
+ OnDestroy,
11
+ signal,
12
+ viewChild,
13
+ } from '@angular/core';
14
+ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
15
+ import { cn } from '../core/utils/cn';
16
+ import { selectTriggerVariants, type SelectSize } from './select.variants';
17
+
18
+ export interface SelectOption {
19
+ value: string;
20
+ label: string;
21
+ }
22
+
23
+ @Component({
24
+ selector: 'ton-select',
25
+ changeDetection: ChangeDetectionStrategy.OnPush,
26
+ host: {
27
+ class: 'relative inline-block w-full',
28
+ '(document:click)': 'onDocumentClick($event)',
29
+ },
30
+ providers: [
31
+ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => TonSelectComponent), multi: true },
32
+ ],
33
+ template: `
34
+ <button
35
+ #triggerEl
36
+ type="button"
37
+ role="combobox"
38
+ [attr.aria-expanded]="open()"
39
+ aria-haspopup="listbox"
40
+ [disabled]="isDisabled()"
41
+ [class]="triggerClass()"
42
+ (click)="toggle()"
43
+ (keydown)="onTriggerKeydown($event)"
44
+ (blur)="onTouched()"
45
+ >
46
+ <span [class]="selectedLabel() ? '' : 'text-muted-foreground'">
47
+ {{ selectedLabel() || placeholder() }}
48
+ </span>
49
+ <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="shrink-0 opacity-50"><path d="m6 9 6 6 6-6"/></svg>
50
+ </button>
51
+
52
+ @if (open()) {
53
+ <div
54
+ #dropdownEl
55
+ class="fixed z-50 rounded-sm border border-border bg-popover text-popover-foreground shadow-md"
56
+ >
57
+ <ul role="listbox" class="max-h-60 overflow-auto p-1 ton-scrollbar">
58
+ @for (opt of options(); track opt.value; let i = $index) {
59
+ <li
60
+ role="option"
61
+ [attr.aria-selected]="value() === opt.value"
62
+ [class]="optionClass(i)"
63
+ (mousedown)="select(opt); $event.preventDefault()"
64
+ (mouseenter)="activeIndex.set(i)"
65
+ >
66
+ <svg
67
+ xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"
68
+ fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
69
+ [class]="value() === opt.value ? 'mr-2 shrink-0 opacity-100' : 'mr-2 shrink-0 opacity-0'"
70
+ ><path d="M20 6 9 17l-5-5"/></svg>
71
+ {{ opt.label }}
72
+ </li>
73
+ }
74
+ </ul>
75
+ </div>
76
+ }
77
+ `,
78
+ })
79
+ export class TonSelectComponent implements ControlValueAccessor, OnDestroy {
80
+ readonly options = input<SelectOption[]>([]);
81
+ readonly placeholder = input('Select...');
82
+ readonly size = input<SelectSize>('md');
83
+ readonly disabled = input(false);
84
+ readonly class = input<string>('');
85
+ readonly value = model<string>('');
86
+
87
+ readonly open = signal(false);
88
+ readonly activeIndex = signal(0);
89
+
90
+ private readonly _disabledByCva = signal(false);
91
+ protected readonly isDisabled = computed(() => this.disabled() || this._disabledByCva());
92
+
93
+ private readonly triggerRef = viewChild<ElementRef<HTMLButtonElement>>('triggerEl');
94
+ private readonly dropdownRef = viewChild<ElementRef<HTMLDivElement>>('dropdownEl');
95
+ private readonly elRef = inject(ElementRef);
96
+
97
+ private scrollHandler: (() => void) | null = null;
98
+ private resizeHandler: (() => void) | null = null;
99
+
100
+ private _onChange: (value: string) => void = () => {};
101
+ protected onTouched: () => void = () => {};
102
+
103
+ writeValue(val: string): void {
104
+ this.value.set(val ?? '');
105
+ }
106
+
107
+ registerOnChange(fn: (value: string) => void): void {
108
+ this._onChange = fn;
109
+ }
110
+
111
+ registerOnTouched(fn: () => void): void {
112
+ this.onTouched = fn;
113
+ }
114
+
115
+ setDisabledState(isDisabled: boolean): void {
116
+ this._disabledByCva.set(isDisabled);
117
+ }
118
+
119
+ readonly selectedLabel = computed(() => {
120
+ const v = this.value();
121
+ if (!v) return '';
122
+ const opt = this.options().find(o => o.value === v);
123
+ return opt?.label ?? '';
124
+ });
125
+
126
+ protected readonly triggerClass = computed(() =>
127
+ cn(selectTriggerVariants({ size: this.size() }), this.class())
128
+ );
129
+
130
+ optionClass(index: number): string {
131
+ const base = 'relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors';
132
+ const active = index === this.activeIndex() ? 'bg-accent text-accent-foreground' : 'hover:bg-accent/50';
133
+ return cn(base, active);
134
+ }
135
+
136
+ private updateDropdownPosition(): void {
137
+ const trigger = this.triggerRef()?.nativeElement;
138
+ if (!trigger) return;
139
+ const rect = trigger.getBoundingClientRect();
140
+ const dropdown = this.dropdownRef()?.nativeElement;
141
+ if (dropdown) {
142
+ dropdown.style.top = `${rect.bottom + 4}px`;
143
+ dropdown.style.left = `${rect.left}px`;
144
+ dropdown.style.width = `${rect.width}px`;
145
+ }
146
+ }
147
+
148
+ private addGlobalListeners(): void {
149
+ this.removeGlobalListeners();
150
+ this.scrollHandler = () => {
151
+ requestAnimationFrame(() => this.updateDropdownPosition());
152
+ };
153
+ this.resizeHandler = () => {
154
+ requestAnimationFrame(() => this.updateDropdownPosition());
155
+ };
156
+ document.addEventListener('scroll', this.scrollHandler, { capture: true, passive: true });
157
+ window.addEventListener('resize', this.resizeHandler, { passive: true });
158
+ }
159
+
160
+ private removeGlobalListeners(): void {
161
+ if (this.scrollHandler) {
162
+ document.removeEventListener('scroll', this.scrollHandler, { capture: true } as EventListenerOptions);
163
+ this.scrollHandler = null;
164
+ }
165
+ if (this.resizeHandler) {
166
+ window.removeEventListener('resize', this.resizeHandler);
167
+ this.resizeHandler = null;
168
+ }
169
+ }
170
+
171
+ ngOnDestroy(): void {
172
+ this.removeGlobalListeners();
173
+ }
174
+
175
+ toggle(): void {
176
+ if (this.open()) {
177
+ this.close();
178
+ } else {
179
+ this.open.set(true);
180
+ this.activeIndex.set(
181
+ Math.max(0, this.options().findIndex(o => o.value === this.value()))
182
+ );
183
+ this.addGlobalListeners();
184
+ setTimeout(() => this.updateDropdownPosition());
185
+ }
186
+ }
187
+
188
+ close(): void {
189
+ this.open.set(false);
190
+ this.removeGlobalListeners();
191
+ }
192
+
193
+ select(opt: SelectOption): void {
194
+ this.value.set(opt.value);
195
+ this._onChange(opt.value);
196
+ this.close();
197
+ }
198
+
199
+ onTriggerKeydown(event: KeyboardEvent): void {
200
+ const items = this.options();
201
+ if (!this.open()) {
202
+ if (event.key === 'ArrowDown' || event.key === 'Enter' || event.key === ' ') {
203
+ event.preventDefault();
204
+ this.toggle();
205
+ }
206
+ return;
207
+ }
208
+ switch (event.key) {
209
+ case 'ArrowDown':
210
+ event.preventDefault();
211
+ this.activeIndex.update(i => Math.min(i + 1, items.length - 1));
212
+ break;
213
+ case 'ArrowUp':
214
+ event.preventDefault();
215
+ this.activeIndex.update(i => Math.max(i - 1, 0));
216
+ break;
217
+ case 'Enter':
218
+ case ' ':
219
+ event.preventDefault();
220
+ if (items[this.activeIndex()]) {
221
+ this.select(items[this.activeIndex()]);
222
+ }
223
+ break;
224
+ case 'Escape':
225
+ this.close();
226
+ break;
227
+ }
228
+ }
229
+
230
+ onDocumentClick(event: MouseEvent): void {
231
+ if (!this.elRef.nativeElement.contains(event.target)) {
232
+ this.close();
233
+ }
234
+ }
235
+ }
@@ -0,0 +1,19 @@
1
+ import { cva } from 'class-variance-authority';
2
+
3
+ export const selectTriggerVariants = cva(
4
+ 'inline-flex w-full items-center justify-between whitespace-nowrap rounded-sm border border-border bg-background px-3 py-2 text-sm ring-offset-background transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
5
+ {
6
+ variants: {
7
+ size: {
8
+ sm: 'h-9 text-xs',
9
+ md: 'h-10 text-sm',
10
+ lg: 'h-11 text-base',
11
+ },
12
+ },
13
+ defaultVariants: {
14
+ size: 'md',
15
+ },
16
+ }
17
+ );
18
+
19
+ export type SelectSize = 'sm' | 'md' | 'lg';
@@ -0,0 +1,10 @@
1
+ export { TonSheetService, TON_SHEET_DATA } from './sheet.service';
2
+ export { TonSheetRef } from './sheet-ref';
3
+ export {
4
+ TonSheetHeaderDirective,
5
+ TonSheetTitleDirective,
6
+ TonSheetDescriptionDirective,
7
+ TonSheetContentDirective,
8
+ TonSheetCloseDirective,
9
+ } from './sheet.directives';
10
+ export { type TonSheetConfig, type SheetSide } from './sheet.types';
@@ -0,0 +1,18 @@
1
+ import type { Observable } from 'rxjs';
2
+
3
+ interface CdkDialogRefLike<R> {
4
+ close(result?: R): void;
5
+ readonly closed: Observable<R | undefined>;
6
+ }
7
+
8
+ export class TonSheetRef<R = unknown> {
9
+ constructor(private readonly cdkRef: CdkDialogRefLike<R>) {}
10
+
11
+ close(result?: R): void {
12
+ this.cdkRef.close(result);
13
+ }
14
+
15
+ get closed(): Observable<R | undefined> {
16
+ return this.cdkRef.closed;
17
+ }
18
+ }
@@ -0,0 +1,67 @@
1
+ import { Component, inject } from '@angular/core';
2
+ import { TestBed } from '@angular/core/testing';
3
+ import { DialogModule, DialogRef } from '@angular/cdk/dialog';
4
+ import { TonSheetService } from './sheet.service';
5
+ import {
6
+ TonSheetHeaderDirective,
7
+ TonSheetTitleDirective,
8
+ TonSheetDescriptionDirective,
9
+ TonSheetCloseDirective,
10
+ } from './sheet.directives';
11
+ import { TonButtonDirective } from '../button/button.directive';
12
+
13
+ @Component({
14
+ standalone: true,
15
+ imports: [
16
+ TonSheetHeaderDirective,
17
+ TonSheetTitleDirective,
18
+ TonSheetDescriptionDirective,
19
+ TonSheetCloseDirective,
20
+ TonButtonDirective,
21
+ ],
22
+ template: `
23
+ <div>
24
+ <div tonSheetHeader>
25
+ <h2 tonSheetTitle>Test Sheet</h2>
26
+ <p tonSheetDescription>A test sheet.</p>
27
+ </div>
28
+ <button tonSheetClose aria-label="Close">X</button>
29
+ <button tonBtn (click)="dialogRef.close('done')">Done</button>
30
+ </div>
31
+ `,
32
+ })
33
+ class TestSheetComponent {
34
+ readonly dialogRef = inject(DialogRef);
35
+ }
36
+
37
+ describe('TonSheetService', () => {
38
+ let service: TonSheetService;
39
+
40
+ beforeEach(async () => {
41
+ await TestBed.configureTestingModule({
42
+ imports: [DialogModule],
43
+ }).compileComponents();
44
+
45
+ service = TestBed.inject(TonSheetService);
46
+ });
47
+
48
+ it('should be created', () => {
49
+ expect(service).toBeTruthy();
50
+ });
51
+
52
+ it('should open a sheet', () => {
53
+ const ref = service.open(TestSheetComponent, { side: 'right' });
54
+ expect(ref).toBeTruthy();
55
+ ref.close();
56
+ });
57
+
58
+ it('should open from different sides', () => {
59
+ const refLeft = service.open(TestSheetComponent, { side: 'left' });
60
+ expect(refLeft).toBeTruthy();
61
+ refLeft.close();
62
+
63
+ const refTop = service.open(TestSheetComponent, { side: 'top' });
64
+ expect(refTop).toBeTruthy();
65
+ refTop.close();
66
+ });
67
+ });
@@ -0,0 +1,70 @@
1
+ import { Directive, computed, input, inject } from '@angular/core';
2
+ import { DialogRef } from '@angular/cdk/dialog';
3
+ import { cn } from '../core/utils/cn';
4
+
5
+ @Directive({
6
+ selector: '[tonSheetHeader]',
7
+ host: { '[class]': 'computedClass()' },
8
+ })
9
+ export class TonSheetHeaderDirective {
10
+ readonly class = input<string>('');
11
+ protected readonly computedClass = computed(() =>
12
+ cn('flex flex-col space-y-2', this.class())
13
+ );
14
+ }
15
+
16
+ @Directive({
17
+ selector: '[tonSheetTitle]',
18
+ host: { '[class]': 'computedClass()' },
19
+ })
20
+ export class TonSheetTitleDirective {
21
+ readonly class = input<string>('');
22
+ protected readonly computedClass = computed(() =>
23
+ cn('text-lg font-semibold text-foreground', this.class())
24
+ );
25
+ }
26
+
27
+ @Directive({
28
+ selector: '[tonSheetDescription]',
29
+ host: { '[class]': 'computedClass()' },
30
+ })
31
+ export class TonSheetDescriptionDirective {
32
+ readonly class = input<string>('');
33
+ protected readonly computedClass = computed(() =>
34
+ cn('text-sm text-muted-foreground', this.class())
35
+ );
36
+ }
37
+
38
+ @Directive({
39
+ selector: '[tonSheetContent]',
40
+ host: { '[class]': 'computedClass()' },
41
+ })
42
+ export class TonSheetContentDirective {
43
+ readonly class = input<string>('');
44
+ protected readonly computedClass = computed(() =>
45
+ cn('py-4', this.class())
46
+ );
47
+ }
48
+
49
+ @Directive({
50
+ selector: '[tonSheetClose]',
51
+ host: {
52
+ '[class]': 'computedClass()',
53
+ '(click)': 'onClick()',
54
+ },
55
+ })
56
+ export class TonSheetCloseDirective {
57
+ readonly class = input<string>('');
58
+ protected readonly computedClass = computed(() =>
59
+ cn(
60
+ 'absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
61
+ this.class()
62
+ )
63
+ );
64
+
65
+ private readonly dialogRef = inject(DialogRef, { optional: true });
66
+
67
+ onClick(): void {
68
+ this.dialogRef?.close();
69
+ }
70
+ }
@@ -0,0 +1,100 @@
1
+ import { Injectable, inject, InjectionToken, Injector } from '@angular/core';
2
+ import { Dialog, DialogRef as CdkDialogRef } from '@angular/cdk/dialog';
3
+ import { type ComponentType, createGlobalPositionStrategy, type GlobalPositionStrategy } from '@angular/cdk/overlay';
4
+ import { TonSheetRef } from './sheet-ref';
5
+ import { DEFAULT_SHEET_CONFIG, SHEET_PANEL_CLASS, type SheetSide, type TonSheetConfig } from './sheet.types';
6
+
7
+ export const TON_SHEET_DATA = new InjectionToken<unknown>('TON_SHEET_DATA');
8
+
9
+ interface SheetOverlayConfig {
10
+ positionStrategy: GlobalPositionStrategy;
11
+ width?: string;
12
+ maxWidth?: string;
13
+ height?: string;
14
+ maxHeight?: string;
15
+ }
16
+
17
+ @Injectable({ providedIn: 'root' })
18
+ export class TonSheetService {
19
+ private readonly cdkDialog = inject(Dialog);
20
+ private readonly injector = inject(Injector);
21
+
22
+ open<T, R = unknown>(component: ComponentType<T>, config: TonSheetConfig = {}): TonSheetRef<R> {
23
+ const merged = { ...DEFAULT_SHEET_CONFIG, ...config };
24
+ const side = merged.side ?? 'right';
25
+ const disableClose = !merged.closeOnBackdrop || !merged.closeOnEsc;
26
+ const overlay = this._getOverlayConfig(side);
27
+
28
+ const cdkRef: CdkDialogRef<R, T> = this.cdkDialog.open(component, {
29
+ disableClose,
30
+ hasBackdrop: true,
31
+ backdropClass: 'ton-dialog-backdrop',
32
+ panelClass: ['ton-sheet-panel', SHEET_PANEL_CLASS[side]],
33
+ positionStrategy: overlay.positionStrategy,
34
+ width: overlay.width,
35
+ maxWidth: overlay.maxWidth,
36
+ height: overlay.height,
37
+ maxHeight: overlay.maxHeight,
38
+ ariaLabelledBy: merged.ariaLabelledBy,
39
+ ariaDescribedBy: merged.ariaDescribedBy,
40
+ data: merged.data,
41
+ providers: merged.data != null
42
+ ? [{ provide: TON_SHEET_DATA, useValue: merged.data }]
43
+ : [],
44
+ });
45
+
46
+ if (disableClose) {
47
+ if (merged.closeOnBackdrop) {
48
+ const sub = cdkRef.backdropClick.subscribe(() => cdkRef.close());
49
+ cdkRef.closed.subscribe(() => sub.unsubscribe());
50
+ }
51
+ if (merged.closeOnEsc) {
52
+ const sub = cdkRef.keydownEvents.subscribe(event => {
53
+ if (event.key === 'Escape') cdkRef.close();
54
+ });
55
+ cdkRef.closed.subscribe(() => sub.unsubscribe());
56
+ }
57
+ }
58
+
59
+ return new TonSheetRef<R>(cdkRef);
60
+ }
61
+
62
+ closeAll(): void {
63
+ this.cdkDialog.closeAll();
64
+ }
65
+
66
+ private _getOverlayConfig(side: SheetSide): SheetOverlayConfig {
67
+ const strategy = createGlobalPositionStrategy(this.injector);
68
+
69
+ switch (side) {
70
+ case 'right':
71
+ return {
72
+ positionStrategy: strategy.top('0').right('0'),
73
+ width: '75%',
74
+ maxWidth: '24rem',
75
+ height: '100vh',
76
+ maxHeight: '100vh',
77
+ };
78
+ case 'left':
79
+ return {
80
+ positionStrategy: strategy.top('0').left('0'),
81
+ width: '75%',
82
+ maxWidth: '24rem',
83
+ height: '100vh',
84
+ maxHeight: '100vh',
85
+ };
86
+ case 'top':
87
+ return {
88
+ positionStrategy: strategy.top('0').centerHorizontally(),
89
+ width: '100vw',
90
+ maxWidth: '100vw',
91
+ };
92
+ case 'bottom':
93
+ return {
94
+ positionStrategy: strategy.bottom('0').centerHorizontally(),
95
+ width: '100vw',
96
+ maxWidth: '100vw',
97
+ };
98
+ }
99
+ }
100
+ }
@@ -0,0 +1,23 @@
1
+ export type SheetSide = 'left' | 'right' | 'top' | 'bottom';
2
+
3
+ export interface TonSheetConfig {
4
+ side?: SheetSide;
5
+ closeOnBackdrop?: boolean;
6
+ closeOnEsc?: boolean;
7
+ data?: unknown;
8
+ ariaLabelledBy?: string;
9
+ ariaDescribedBy?: string;
10
+ }
11
+
12
+ export const DEFAULT_SHEET_CONFIG: TonSheetConfig = {
13
+ side: 'right',
14
+ closeOnBackdrop: true,
15
+ closeOnEsc: true,
16
+ };
17
+
18
+ export const SHEET_PANEL_CLASS: Record<SheetSide, string> = {
19
+ right: 'ton-sheet-right',
20
+ left: 'ton-sheet-left',
21
+ top: 'ton-sheet-top',
22
+ bottom: 'ton-sheet-bottom',
23
+ };
@@ -0,0 +1,2 @@
1
+ export { TonSkeletonDirective } from './skeleton.directive';
2
+ export { skeletonVariants, type SkeletonVariant, type SkeletonSize } from './skeleton.variants';