@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,52 @@
1
+ import { Component } from '@angular/core';
2
+ import { TestBed } from '@angular/core/testing';
3
+ import { DialogModule } from '@angular/cdk/dialog';
4
+ import { TonDialogService } from './dialog.service';
5
+
6
+ @Component({
7
+ standalone: true,
8
+ template: `<div>Dialog Content</div>`,
9
+ })
10
+ class TestDialogComponent {}
11
+
12
+ describe('TonDialogService', () => {
13
+ let service: TonDialogService;
14
+
15
+ beforeEach(() => {
16
+ TestBed.configureTestingModule({
17
+ imports: [DialogModule],
18
+ });
19
+ service = TestBed.inject(TonDialogService);
20
+ });
21
+
22
+ afterEach(() => {
23
+ service.closeAll();
24
+ });
25
+
26
+ it('should open a dialog', () => {
27
+ const ref = service.open(TestDialogComponent);
28
+ expect(ref).toBeTruthy();
29
+ ref.close();
30
+ });
31
+
32
+ it('should close a dialog', async () => {
33
+ const ref = service.open(TestDialogComponent);
34
+ let closed = false;
35
+ ref.closed.subscribe(() => (closed = true));
36
+ ref.close();
37
+ await new Promise(resolve => setTimeout(resolve, 50));
38
+ expect(closed).toBe(true);
39
+ });
40
+
41
+ it('should close all dialogs', () => {
42
+ service.open(TestDialogComponent);
43
+ service.open(TestDialogComponent);
44
+ service.closeAll();
45
+ });
46
+
47
+ it('should accept width config', () => {
48
+ const ref = service.open(TestDialogComponent, { width: '600px' });
49
+ expect(ref).toBeTruthy();
50
+ ref.close();
51
+ });
52
+ });
@@ -0,0 +1,61 @@
1
+ import { Injectable, inject, InjectionToken } from '@angular/core';
2
+ import { Dialog, DialogRef as CdkDialogRef } from '@angular/cdk/dialog';
3
+ import type { ComponentType } from '@angular/cdk/overlay';
4
+ import { TonDialogRef } from './dialog-ref';
5
+ import { DEFAULT_DIALOG_CONFIG, type TonDialogConfig } from './dialog.types';
6
+
7
+ export const TON_DIALOG_DATA = new InjectionToken<unknown>('TON_DIALOG_DATA');
8
+
9
+ @Injectable({ providedIn: 'root' })
10
+ export class TonDialogService {
11
+ private readonly cdkDialog = inject(Dialog);
12
+
13
+ open<T, R = unknown>(
14
+ component: ComponentType<T>,
15
+ config: TonDialogConfig = {}
16
+ ): TonDialogRef<R> {
17
+ const merged = { ...DEFAULT_DIALOG_CONFIG, ...config };
18
+
19
+ // CDK's disableClose controls both backdrop and ESC together.
20
+ // To support independent closeOnBackdrop / closeOnEsc, we disable both
21
+ // at the CDK level and handle them manually.
22
+ const disableClose = !merged.closeOnBackdrop || !merged.closeOnEsc;
23
+
24
+ const cdkRef: CdkDialogRef<R, T> = this.cdkDialog.open(component, {
25
+ width: merged.width,
26
+ maxWidth: merged.maxWidth,
27
+ disableClose,
28
+ hasBackdrop: true,
29
+ backdropClass: 'ton-dialog-backdrop',
30
+ panelClass: 'ton-dialog-panel',
31
+ ariaLabelledBy: merged.ariaLabelledBy,
32
+ ariaDescribedBy: merged.ariaDescribedBy,
33
+ data: merged.data,
34
+ providers: merged.data != null
35
+ ? [{ provide: TON_DIALOG_DATA, useValue: merged.data }]
36
+ : [],
37
+ });
38
+
39
+ // When CDK disableClose is true, manually handle backdrop/ESC based on config
40
+ if (disableClose) {
41
+ if (merged.closeOnBackdrop) {
42
+ const sub = cdkRef.backdropClick.subscribe(() => cdkRef.close());
43
+ cdkRef.closed.subscribe(() => sub.unsubscribe());
44
+ }
45
+ if (merged.closeOnEsc) {
46
+ const sub = cdkRef.keydownEvents.subscribe(event => {
47
+ if (event.key === 'Escape') {
48
+ cdkRef.close();
49
+ }
50
+ });
51
+ cdkRef.closed.subscribe(() => sub.unsubscribe());
52
+ }
53
+ }
54
+
55
+ return new TonDialogRef<R>(cdkRef);
56
+ }
57
+
58
+ closeAll(): void {
59
+ this.cdkDialog.closeAll();
60
+ }
61
+ }
@@ -0,0 +1,16 @@
1
+ export interface TonDialogConfig {
2
+ width?: string;
3
+ maxWidth?: string;
4
+ closeOnBackdrop?: boolean;
5
+ closeOnEsc?: boolean;
6
+ data?: unknown;
7
+ ariaLabelledBy?: string;
8
+ ariaDescribedBy?: string;
9
+ }
10
+
11
+ export const DEFAULT_DIALOG_CONFIG: TonDialogConfig = {
12
+ width: '28rem',
13
+ maxWidth: '90vw',
14
+ closeOnBackdrop: true,
15
+ closeOnEsc: true,
16
+ };
@@ -0,0 +1,11 @@
1
+ export { TonDialogService, TON_DIALOG_DATA } from './dialog.service';
2
+ export { TonDialogRef } from './dialog-ref';
3
+ export {
4
+ TonDialogHeaderDirective,
5
+ TonDialogTitleDirective,
6
+ TonDialogDescriptionDirective,
7
+ TonDialogContentDirective,
8
+ TonDialogFooterDirective,
9
+ TonDialogCloseDirective,
10
+ } from './dialog.directives';
11
+ export { type TonDialogConfig } from './dialog.types';
@@ -0,0 +1,7 @@
1
+ export {
2
+ TonNavbarDirective,
3
+ TonNavbarBrandDirective,
4
+ TonNavbarContentDirective,
5
+ TonNavbarEndDirective,
6
+ type NavbarVariant,
7
+ } from './navbar.directives';
@@ -0,0 +1,59 @@
1
+ import { Component, signal } from '@angular/core';
2
+ import { TestBed, type ComponentFixture } from '@angular/core/testing';
3
+ import { TonNavbarDirective, TonNavbarBrandDirective, TonNavbarContentDirective, TonNavbarEndDirective, type NavbarVariant } from './navbar.directives';
4
+
5
+ @Component({
6
+ standalone: true,
7
+ imports: [TonNavbarDirective, TonNavbarBrandDirective, TonNavbarContentDirective, TonNavbarEndDirective],
8
+ template: `
9
+ <nav tonNavbar [variant]="variant()" [sticky]="sticky()">
10
+ <div tonNavbarBrand>Logo</div>
11
+ <div tonNavbarContent>Links</div>
12
+ <div tonNavbarEnd>Actions</div>
13
+ </nav>
14
+ `,
15
+ })
16
+ class TestHostComponent {
17
+ variant = signal<NavbarVariant>('default');
18
+ sticky = signal(false);
19
+ }
20
+
21
+ describe('TonNavbarDirective', () => {
22
+ let fixture: ComponentFixture<TestHostComponent>;
23
+ let nav: HTMLElement;
24
+
25
+ beforeEach(async () => {
26
+ await TestBed.configureTestingModule({ imports: [TestHostComponent] }).compileComponents();
27
+ fixture = TestBed.createComponent(TestHostComponent);
28
+ fixture.detectChanges();
29
+ nav = fixture.nativeElement.querySelector('[tonNavbar]');
30
+ });
31
+
32
+ it('should apply default classes', () => {
33
+ expect(nav.className).toContain('bg-background');
34
+ expect(nav.className).toContain('flex');
35
+ });
36
+
37
+ it('should apply bordered variant', () => {
38
+ fixture.componentInstance.variant.set('bordered');
39
+ fixture.detectChanges();
40
+ expect(nav.className).toContain('border-b');
41
+ });
42
+
43
+ it('should apply sticky class', () => {
44
+ fixture.componentInstance.sticky.set(true);
45
+ fixture.detectChanges();
46
+ expect(nav.className).toContain('sticky');
47
+ expect(nav.className).toContain('top-0');
48
+ });
49
+
50
+ it('should render brand, content, end sections', () => {
51
+ expect(fixture.nativeElement.querySelector('[tonNavbarBrand]')).toBeTruthy();
52
+ expect(fixture.nativeElement.querySelector('[tonNavbarContent]')).toBeTruthy();
53
+ expect(fixture.nativeElement.querySelector('[tonNavbarEnd]')).toBeTruthy();
54
+ });
55
+
56
+ it('should have default aria-label "Main navigation"', () => {
57
+ expect(nav.getAttribute('aria-label')).toBe('Main navigation');
58
+ });
59
+ });
@@ -0,0 +1,57 @@
1
+ import { Directive, computed, input } from '@angular/core';
2
+ import { cn } from '../core/utils/cn';
3
+
4
+ export type NavbarVariant = 'default' | 'bordered' | 'floating';
5
+
6
+ @Directive({
7
+ selector: '[tonNavbar]',
8
+ host: {
9
+ 'role': 'navigation',
10
+ '[attr.aria-label]': 'ariaLabel()',
11
+ '[class]': 'computedClass()',
12
+ },
13
+ })
14
+ export class TonNavbarDirective {
15
+ readonly variant = input<NavbarVariant>('default');
16
+ readonly sticky = input(false);
17
+ readonly ariaLabel = input<string>('Main navigation');
18
+ readonly class = input<string>('');
19
+
20
+ protected readonly computedClass = computed(() => {
21
+ const v = this.variant();
22
+ const base = 'flex items-center justify-between px-4 py-2 w-full bg-background';
23
+ const variantClass =
24
+ v === 'bordered' ? 'border-b border-border' :
25
+ v === 'floating' ? 'mx-4 mt-2 rounded-lg border border-border shadow-sm' :
26
+ '';
27
+ const stickyClass = this.sticky() ? 'sticky top-0 z-40' : '';
28
+ return cn(base, variantClass, stickyClass, this.class());
29
+ });
30
+ }
31
+
32
+ @Directive({
33
+ selector: '[tonNavbarBrand]',
34
+ host: { '[class]': 'computedClass()' },
35
+ })
36
+ export class TonNavbarBrandDirective {
37
+ readonly class = input<string>('');
38
+ protected readonly computedClass = computed(() => cn('flex items-center gap-2 font-bold text-lg', this.class()));
39
+ }
40
+
41
+ @Directive({
42
+ selector: '[tonNavbarContent]',
43
+ host: { '[class]': 'computedClass()' },
44
+ })
45
+ export class TonNavbarContentDirective {
46
+ readonly class = input<string>('');
47
+ protected readonly computedClass = computed(() => cn('flex items-center gap-1', this.class()));
48
+ }
49
+
50
+ @Directive({
51
+ selector: '[tonNavbarEnd]',
52
+ host: { '[class]': 'computedClass()' },
53
+ })
54
+ export class TonNavbarEndDirective {
55
+ readonly class = input<string>('');
56
+ protected readonly computedClass = computed(() => cn('flex items-center gap-2 ml-auto', this.class()));
57
+ }
@@ -0,0 +1,2 @@
1
+ export { TonNumberInputComponent } from './number-input.component';
2
+ export { numberInputVariants, type NumberInputSize } from './number-input.variants';
@@ -0,0 +1,151 @@
1
+ import { Component, signal } from '@angular/core';
2
+ import { FormControl, ReactiveFormsModule } from '@angular/forms';
3
+ import { TestBed, type ComponentFixture } from '@angular/core/testing';
4
+ import { TonNumberInputComponent } from './number-input.component';
5
+
6
+ @Component({
7
+ standalone: true,
8
+ imports: [TonNumberInputComponent],
9
+ template: `
10
+ <ton-number-input
11
+ [(value)]="num"
12
+ [min]="min()"
13
+ [max]="max()"
14
+ [step]="step()"
15
+ [disabled]="disabled()"
16
+ />
17
+ `,
18
+ })
19
+ class TestHost {
20
+ num = signal(5);
21
+ min = signal<number | null>(null);
22
+ max = signal<number | null>(null);
23
+ step = signal(1);
24
+ disabled = signal(false);
25
+ }
26
+
27
+ describe('TonNumberInputComponent', () => {
28
+ let fixture: ComponentFixture<TestHost>;
29
+ let el: HTMLElement;
30
+
31
+ beforeEach(async () => {
32
+ await TestBed.configureTestingModule({ imports: [TestHost] }).compileComponents();
33
+ fixture = TestBed.createComponent(TestHost);
34
+ fixture.detectChanges();
35
+ el = fixture.nativeElement;
36
+ });
37
+
38
+ function getInput(): HTMLInputElement {
39
+ return el.querySelector('input') as HTMLInputElement;
40
+ }
41
+ function getButtons(): HTMLButtonElement[] {
42
+ return Array.from(el.querySelectorAll('button'));
43
+ }
44
+
45
+ it('should render with initial value', () => {
46
+ expect(getInput().value).toBe('5');
47
+ });
48
+
49
+ it('should increment on + click', () => {
50
+ getButtons()[1].click();
51
+ fixture.detectChanges();
52
+ expect(fixture.componentInstance.num()).toBe(6);
53
+ });
54
+
55
+ it('should decrement on - click', () => {
56
+ getButtons()[0].click();
57
+ fixture.detectChanges();
58
+ expect(fixture.componentInstance.num()).toBe(4);
59
+ });
60
+
61
+ it('should respect min', () => {
62
+ fixture.componentInstance.min.set(5);
63
+ fixture.detectChanges();
64
+ getButtons()[0].click();
65
+ fixture.detectChanges();
66
+ expect(fixture.componentInstance.num()).toBe(5);
67
+ });
68
+
69
+ it('should respect max', () => {
70
+ fixture.componentInstance.max.set(5);
71
+ fixture.detectChanges();
72
+ getButtons()[1].click();
73
+ fixture.detectChanges();
74
+ expect(fixture.componentInstance.num()).toBe(5);
75
+ });
76
+
77
+ it('should use step', () => {
78
+ fixture.componentInstance.step.set(10);
79
+ fixture.detectChanges();
80
+ getButtons()[1].click();
81
+ fixture.detectChanges();
82
+ expect(fixture.componentInstance.num()).toBe(15); // 5 + 10
83
+ });
84
+
85
+ it('should handle manual input on blur', () => {
86
+ const input = getInput();
87
+ input.value = '42';
88
+ input.dispatchEvent(new Event('input'));
89
+ input.dispatchEvent(new Event('blur'));
90
+ fixture.detectChanges();
91
+ expect(fixture.componentInstance.num()).toBe(42);
92
+ });
93
+
94
+ it('should revert invalid input on blur', () => {
95
+ const input = getInput();
96
+ input.value = 'abc';
97
+ input.dispatchEvent(new Event('input', { bubbles: true }));
98
+ input.dispatchEvent(new Event('blur', { bubbles: true }));
99
+ fixture.detectChanges();
100
+ // Value should remain unchanged at 5
101
+ expect(fixture.componentInstance.num()).toBe(5);
102
+ });
103
+
104
+ it('should handle ArrowUp/Down', () => {
105
+ const input = getInput();
106
+ input.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowUp', bubbles: true }));
107
+ fixture.detectChanges();
108
+ expect(fixture.componentInstance.num()).toBe(6);
109
+ input.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }));
110
+ fixture.detectChanges();
111
+ expect(fixture.componentInstance.num()).toBe(5);
112
+ });
113
+
114
+ it('should disable when disabled', () => {
115
+ fixture.componentInstance.disabled.set(true);
116
+ fixture.detectChanges();
117
+ expect(getInput().disabled).toBe(true);
118
+ expect(getButtons().every(b => b.disabled)).toBe(true);
119
+ });
120
+ });
121
+
122
+ @Component({
123
+ standalone: true,
124
+ imports: [ReactiveFormsModule, TonNumberInputComponent],
125
+ template: `<ton-number-input [formControl]="ctrl" />`,
126
+ })
127
+ class ReactiveHost {
128
+ ctrl = new FormControl(10);
129
+ }
130
+
131
+ describe('TonNumberInputComponent — Reactive Forms', () => {
132
+ let fixture: ComponentFixture<ReactiveHost>;
133
+
134
+ beforeEach(async () => {
135
+ await TestBed.configureTestingModule({ imports: [ReactiveHost] }).compileComponents();
136
+ fixture = TestBed.createComponent(ReactiveHost);
137
+ fixture.detectChanges();
138
+ });
139
+
140
+ it('should display FormControl value', () => {
141
+ const input = fixture.nativeElement.querySelector('input') as HTMLInputElement;
142
+ expect(input.value).toBe('10');
143
+ });
144
+
145
+ it('should update FormControl on increment', () => {
146
+ const buttons = fixture.nativeElement.querySelectorAll('button');
147
+ buttons[1].click();
148
+ fixture.detectChanges();
149
+ expect(fixture.componentInstance.ctrl.value).toBe(11);
150
+ });
151
+ });
@@ -0,0 +1,152 @@
1
+ import {
2
+ ChangeDetectionStrategy,
3
+ Component,
4
+ computed,
5
+ effect,
6
+ forwardRef,
7
+ input,
8
+ model,
9
+ signal,
10
+ untracked,
11
+ } from '@angular/core';
12
+ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
13
+ import { cn } from '../core/utils/cn';
14
+ import { numberInputVariants, type NumberInputSize } from './number-input.variants';
15
+
16
+ @Component({
17
+ selector: 'ton-number-input',
18
+ changeDetection: ChangeDetectionStrategy.OnPush,
19
+ providers: [
20
+ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => TonNumberInputComponent), multi: true },
21
+ ],
22
+ template: `
23
+ <div [class]="containerClass()">
24
+ <button
25
+ type="button"
26
+ class="px-2.5 hover:bg-accent transition-colors border-r border-border disabled:opacity-40 disabled:cursor-not-allowed"
27
+ [disabled]="isDisabled() || atMin()"
28
+ (click)="decrement()"
29
+ aria-label="Decrease"
30
+ >
31
+ <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/></svg>
32
+ </button>
33
+ <input
34
+ #inputEl
35
+ type="text"
36
+ inputmode="decimal"
37
+ class="flex-1 w-14 text-center outline-none bg-transparent font-medium"
38
+ [value]="inputValue()"
39
+ [disabled]="isDisabled()"
40
+ [placeholder]="placeholder()"
41
+ [attr.aria-label]="'Number input'"
42
+ [attr.aria-valuemin]="min() ?? null"
43
+ [attr.aria-valuemax]="max() ?? null"
44
+ [attr.aria-valuenow]="value()"
45
+ role="spinbutton"
46
+ (input)="onInput($event)"
47
+ (blur)="commitValue()"
48
+ (keydown)="onKeydown($event)"
49
+ />
50
+ <button
51
+ type="button"
52
+ class="px-2.5 hover:bg-accent transition-colors border-l border-border disabled:opacity-40 disabled:cursor-not-allowed"
53
+ [disabled]="isDisabled() || atMax()"
54
+ (click)="increment()"
55
+ aria-label="Increase"
56
+ >
57
+ <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="M12 5v14"/></svg>
58
+ </button>
59
+ </div>
60
+ `,
61
+ })
62
+ export class TonNumberInputComponent implements ControlValueAccessor {
63
+ readonly value = model(0);
64
+ readonly min = input<number | null>(null);
65
+ readonly max = input<number | null>(null);
66
+ readonly step = input(1);
67
+ readonly disabled = input(false);
68
+ readonly size = input<NumberInputSize>('md');
69
+ readonly placeholder = input('');
70
+ readonly class = input<string>('');
71
+
72
+ readonly inputValue = signal('0');
73
+ private readonly _disabledByCva = signal(false);
74
+ readonly isDisabled = computed(() => this.disabled() || this._disabledByCva());
75
+
76
+ readonly atMin = computed(() => this.min() !== null && this.value() <= this.min()!);
77
+ readonly atMax = computed(() => this.max() !== null && this.value() >= this.max()!);
78
+
79
+ readonly containerClass = computed(() =>
80
+ cn(numberInputVariants({ size: this.size() }), this.isDisabled() && 'opacity-50', this.class())
81
+ );
82
+
83
+ private _onChange: (value: number) => void = () => {};
84
+ private _onTouched: () => void = () => {};
85
+
86
+ constructor() {
87
+ effect(() => {
88
+ const val = this.value();
89
+ untracked(() => this.inputValue.set(String(val)));
90
+ });
91
+ }
92
+
93
+ writeValue(val: number): void {
94
+ this.value.set(val ?? 0);
95
+ }
96
+
97
+ registerOnChange(fn: (value: number) => void): void {
98
+ this._onChange = fn;
99
+ }
100
+
101
+ registerOnTouched(fn: () => void): void {
102
+ this._onTouched = fn;
103
+ }
104
+
105
+ setDisabledState(isDisabled: boolean): void {
106
+ this._disabledByCva.set(isDisabled);
107
+ }
108
+
109
+ increment(): void {
110
+ this.setValue(this.value() + this.step());
111
+ }
112
+
113
+ decrement(): void {
114
+ this.setValue(this.value() - this.step());
115
+ }
116
+
117
+ onInput(event: Event): void {
118
+ this.inputValue.set((event.target as HTMLInputElement).value);
119
+ }
120
+
121
+ commitValue(): void {
122
+ const parsed = parseFloat(this.inputValue());
123
+ if (isNaN(parsed)) {
124
+ this.inputValue.set(String(this.value()));
125
+ } else {
126
+ this.setValue(parsed);
127
+ }
128
+ this._onTouched();
129
+ }
130
+
131
+ onKeydown(event: KeyboardEvent): void {
132
+ switch (event.key) {
133
+ case 'ArrowUp':
134
+ event.preventDefault();
135
+ this.increment();
136
+ break;
137
+ case 'ArrowDown':
138
+ event.preventDefault();
139
+ this.decrement();
140
+ break;
141
+ }
142
+ }
143
+
144
+ private setValue(raw: number): void {
145
+ let clamped = raw;
146
+ if (this.min() !== null) clamped = Math.max(this.min()!, clamped);
147
+ if (this.max() !== null) clamped = Math.min(this.max()!, clamped);
148
+ const rounded = parseFloat(clamped.toFixed(10));
149
+ this.value.set(rounded);
150
+ this._onChange(rounded);
151
+ }
152
+ }
@@ -0,0 +1,17 @@
1
+ import { cva } from 'class-variance-authority';
2
+
3
+ export const numberInputVariants = cva(
4
+ 'inline-flex items-center border border-border rounded-md bg-background transition-colors focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2',
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: { size: 'md' },
14
+ }
15
+ );
16
+
17
+ export type NumberInputSize = 'sm' | 'md' | 'lg';
@@ -0,0 +1,2 @@
1
+ export { TonOtpInputComponent } from './otp-input.component';
2
+ export { otpCellVariants, type OtpInputSize, type OtpInputType } from './otp-input.variants';