@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,43 @@
1
+ import { ChangeDetectionStrategy, Component, computed, input, signal } from '@angular/core';
2
+ import { cn } from '../core/utils/cn';
3
+ import { avatarVariants, type AvatarSize, type AvatarVariant } from './avatar.variants';
4
+
5
+ @Component({
6
+ selector: 'ton-avatar',
7
+ changeDetection: ChangeDetectionStrategy.OnPush,
8
+ host: { '[class]': 'computedClass()' },
9
+ template: `
10
+ @if (src() && !error()) {
11
+ <img
12
+ [src]="src()"
13
+ [alt]="alt()"
14
+ class="aspect-square h-full w-full object-cover"
15
+ (error)="error.set(true)"
16
+ />
17
+ } @else {
18
+ <span class="font-medium text-muted-foreground">{{ fallbackText() }}</span>
19
+ }
20
+ `,
21
+ })
22
+ export class TonAvatarComponent {
23
+ readonly src = input<string>('');
24
+ readonly alt = input<string>('');
25
+ readonly fallback = input<string>('');
26
+ readonly size = input<AvatarSize>('md');
27
+ readonly variant = input<AvatarVariant>('circle');
28
+ readonly class = input<string>('');
29
+
30
+ readonly error = signal(false);
31
+
32
+ protected readonly fallbackText = computed(() => {
33
+ const fb = this.fallback();
34
+ if (fb) return fb;
35
+ const a = this.alt();
36
+ if (a) return a.split(' ').map(w => w[0]).join('').toUpperCase().slice(0, 2);
37
+ return '?';
38
+ });
39
+
40
+ protected readonly computedClass = computed(() =>
41
+ cn(avatarVariants({ size: this.size(), variant: this.variant() }), this.class())
42
+ );
43
+ }
@@ -0,0 +1,26 @@
1
+ import { cva } from 'class-variance-authority';
2
+
3
+ export const avatarVariants = cva(
4
+ 'relative inline-flex shrink-0 items-center justify-center overflow-hidden bg-muted',
5
+ {
6
+ variants: {
7
+ size: {
8
+ sm: 'h-8 w-8 text-xs',
9
+ md: 'h-10 w-10 text-sm',
10
+ lg: 'h-12 w-12 text-base',
11
+ xl: 'h-16 w-16 text-lg',
12
+ },
13
+ variant: {
14
+ circle: 'rounded-full',
15
+ rounded: 'rounded-md',
16
+ },
17
+ },
18
+ defaultVariants: {
19
+ size: 'md',
20
+ variant: 'circle',
21
+ },
22
+ }
23
+ );
24
+
25
+ export type AvatarSize = 'sm' | 'md' | 'lg' | 'xl';
26
+ export type AvatarVariant = 'circle' | 'rounded';
@@ -0,0 +1,2 @@
1
+ export { TonAvatarComponent } from './avatar.component';
2
+ export { avatarVariants, type AvatarSize, type AvatarVariant } from './avatar.variants';
@@ -0,0 +1,74 @@
1
+ import { Component, signal } from '@angular/core';
2
+ import { TestBed, type ComponentFixture } from '@angular/core/testing';
3
+ import { TonAvatarGroupComponent, type AvatarGroupItem } from './avatar-group.component';
4
+
5
+ @Component({
6
+ standalone: true,
7
+ imports: [TonAvatarGroupComponent],
8
+ template: `<ton-avatar-group [items]="items()" [max]="max()" [size]="size()" />`,
9
+ })
10
+ class TestHost {
11
+ items = signal<AvatarGroupItem[]>([
12
+ { src: 'a.jpg', alt: 'Alice' },
13
+ { src: 'b.jpg', alt: 'Bob' },
14
+ { src: 'c.jpg', alt: 'Carol' },
15
+ { src: 'd.jpg', alt: 'David' },
16
+ { src: 'e.jpg', alt: 'Eve' },
17
+ ]);
18
+ max = signal(3);
19
+ size = signal<'sm' | 'md' | 'lg'>('md');
20
+ }
21
+
22
+ describe('TonAvatarGroupComponent', () => {
23
+ let fixture: ComponentFixture<TestHost>;
24
+ let el: HTMLElement;
25
+
26
+ beforeEach(async () => {
27
+ await TestBed.configureTestingModule({ imports: [TestHost] }).compileComponents();
28
+ fixture = TestBed.createComponent(TestHost);
29
+ fixture.detectChanges();
30
+ el = fixture.nativeElement;
31
+ });
32
+
33
+ it('should render max avatars', () => {
34
+ const imgs = el.querySelectorAll('img');
35
+ expect(imgs.length).toBe(3);
36
+ });
37
+
38
+ it('should show overflow counter', () => {
39
+ const counter = el.querySelector('[title="2 more"]');
40
+ expect(counter).not.toBeNull();
41
+ expect(counter?.textContent).toContain('+2');
42
+ });
43
+
44
+ it('should not show counter when no overflow', () => {
45
+ fixture.componentInstance.max.set(5);
46
+ fixture.detectChanges();
47
+ const counter = el.querySelector('[title]');
48
+ expect(counter).toBeNull();
49
+ });
50
+
51
+ it('should render all when max >= items', () => {
52
+ fixture.componentInstance.max.set(10);
53
+ fixture.detectChanges();
54
+ const imgs = el.querySelectorAll('img');
55
+ expect(imgs.length).toBe(5);
56
+ });
57
+
58
+ it('should render fallback initials when no src', () => {
59
+ fixture.componentInstance.items.set([
60
+ { fallback: 'AB' },
61
+ { fallback: 'CD' },
62
+ ]);
63
+ fixture.componentInstance.max.set(2);
64
+ fixture.detectChanges();
65
+ const fallbacks = el.querySelectorAll('.bg-muted');
66
+ expect(fallbacks.length).toBe(2);
67
+ expect(fallbacks[0].textContent).toContain('AB');
68
+ });
69
+
70
+ it('should have aria-label on group', () => {
71
+ const group = el.querySelector('[role="group"]');
72
+ expect(group?.getAttribute('aria-label')).toBe('Group of 5 users');
73
+ });
74
+ });
@@ -0,0 +1,88 @@
1
+ import {
2
+ ChangeDetectionStrategy,
3
+ Component,
4
+ computed,
5
+ input,
6
+ } from '@angular/core';
7
+ import { cn } from '../core/utils/cn';
8
+
9
+ export interface AvatarGroupItem {
10
+ src?: string;
11
+ alt?: string;
12
+ fallback?: string;
13
+ }
14
+
15
+ const sizeMap = {
16
+ sm: { avatar: 'h-7 w-7 text-xs', counter: 'h-7 w-7 text-[10px]' },
17
+ md: { avatar: 'h-9 w-9 text-sm', counter: 'h-9 w-9 text-xs' },
18
+ lg: { avatar: 'h-11 w-11 text-base', counter: 'h-11 w-11 text-sm' },
19
+ };
20
+
21
+ const spacingMap = {
22
+ tight: '-space-x-3',
23
+ normal: '-space-x-2',
24
+ };
25
+
26
+ export type AvatarGroupSize = 'sm' | 'md' | 'lg';
27
+ export type AvatarGroupSpacing = 'tight' | 'normal';
28
+
29
+ @Component({
30
+ selector: 'ton-avatar-group',
31
+ changeDetection: ChangeDetectionStrategy.OnPush,
32
+ template: `
33
+ <div [class]="containerClass()" role="group" [attr.aria-label]="'Group of ' + items().length + ' users'">
34
+ @for (item of visibleItems(); track $index) {
35
+ @if (item.src) {
36
+ <img
37
+ [src]="item.src"
38
+ [alt]="item.alt ?? ''"
39
+ [class]="avatarClass()"
40
+ />
41
+ } @else {
42
+ <div [class]="fallbackClass()">
43
+ {{ item.fallback ?? '?' }}
44
+ </div>
45
+ }
46
+ }
47
+ @if (overflowCount() > 0) {
48
+ <div [class]="counterClass()" [title]="overflowCount() + ' more'">
49
+ +{{ overflowCount() }}
50
+ </div>
51
+ }
52
+ </div>
53
+ `,
54
+ })
55
+ export class TonAvatarGroupComponent {
56
+ readonly items = input.required<AvatarGroupItem[]>();
57
+ readonly max = input(3);
58
+ readonly size = input<AvatarGroupSize>('md');
59
+ readonly spacing = input<AvatarGroupSpacing>('normal');
60
+
61
+ readonly visibleItems = computed(() => this.items().slice(0, this.max()));
62
+ readonly overflowCount = computed(() => Math.max(0, this.items().length - this.max()));
63
+
64
+ readonly containerClass = computed(() =>
65
+ cn('flex items-center', spacingMap[this.spacing()])
66
+ );
67
+
68
+ readonly avatarClass = computed(() =>
69
+ cn(
70
+ 'inline-block rounded-full object-cover ring-2 ring-background',
71
+ sizeMap[this.size()].avatar
72
+ )
73
+ );
74
+
75
+ readonly fallbackClass = computed(() =>
76
+ cn(
77
+ 'inline-flex items-center justify-center rounded-full bg-muted text-muted-foreground font-medium ring-2 ring-background',
78
+ sizeMap[this.size()].avatar
79
+ )
80
+ );
81
+
82
+ readonly counterClass = computed(() =>
83
+ cn(
84
+ 'inline-flex items-center justify-center rounded-full bg-muted text-muted-foreground font-semibold ring-2 ring-background',
85
+ sizeMap[this.size()].counter
86
+ )
87
+ );
88
+ }
@@ -0,0 +1 @@
1
+ export { TonAvatarGroupComponent, type AvatarGroupItem, type AvatarGroupSize, type AvatarGroupSpacing } from './avatar-group.component';
@@ -0,0 +1,74 @@
1
+ import { Component, signal } from '@angular/core';
2
+ import { TestBed, ComponentFixture } from '@angular/core/testing';
3
+ import { TonBadgeDirective } from './badge.directive';
4
+ import type { BadgeVariant, BadgeSize } from './badge.variants';
5
+
6
+ @Component({
7
+ standalone: true,
8
+ imports: [TonBadgeDirective],
9
+ template: `<span tonBadge [variant]="variant()" [size]="size()">Badge</span>`,
10
+ })
11
+ class TestHostComponent {
12
+ variant = signal<BadgeVariant>('default');
13
+ size = signal<BadgeSize>('md');
14
+ }
15
+
16
+ describe('TonBadgeDirective', () => {
17
+ let fixture: ComponentFixture<TestHostComponent>;
18
+ let el: HTMLElement;
19
+
20
+ beforeEach(async () => {
21
+ await TestBed.configureTestingModule({
22
+ imports: [TestHostComponent],
23
+ }).compileComponents();
24
+
25
+ fixture = TestBed.createComponent(TestHostComponent);
26
+ fixture.detectChanges();
27
+ el = fixture.nativeElement.querySelector('[tonBadge]');
28
+ });
29
+
30
+ it('should apply default variant classes', () => {
31
+ expect(el.className).toContain('bg-primary');
32
+ expect(el.className).toContain('text-primary-foreground');
33
+ });
34
+
35
+ it('should apply destructive variant', () => {
36
+ fixture.componentInstance.variant.set('destructive');
37
+ fixture.detectChanges();
38
+ expect(el.className).toContain('bg-destructive');
39
+ });
40
+
41
+ it('should apply outline variant', () => {
42
+ fixture.componentInstance.variant.set('outline');
43
+ fixture.detectChanges();
44
+ expect(el.className).toContain('border-border');
45
+ });
46
+
47
+ it('should apply success variant', () => {
48
+ fixture.componentInstance.variant.set('success');
49
+ fixture.detectChanges();
50
+ expect(el.className).toContain('bg-green-600');
51
+ });
52
+
53
+ it('should apply warning variant', () => {
54
+ fixture.componentInstance.variant.set('warning');
55
+ fixture.detectChanges();
56
+ expect(el.className).toContain('bg-yellow-500');
57
+ });
58
+
59
+ it('should apply sm size', () => {
60
+ fixture.componentInstance.size.set('sm');
61
+ fixture.detectChanges();
62
+ expect(el.className).toContain('text-[10px]');
63
+ });
64
+
65
+ it('should apply lg size', () => {
66
+ fixture.componentInstance.size.set('lg');
67
+ fixture.detectChanges();
68
+ expect(el.className).toContain('text-sm');
69
+ });
70
+
71
+ it('should have rounded-full class', () => {
72
+ expect(el.className).toContain('rounded-full');
73
+ });
74
+ });
@@ -0,0 +1,17 @@
1
+ import { Directive, computed, input } from '@angular/core';
2
+ import { cn } from '../core/utils/cn';
3
+ import { badgeVariants, type BadgeVariant, type BadgeSize } from './badge.variants';
4
+
5
+ @Directive({
6
+ selector: '[tonBadge]',
7
+ host: { '[class]': 'computedClass()' },
8
+ })
9
+ export class TonBadgeDirective {
10
+ readonly variant = input<BadgeVariant>('default');
11
+ readonly size = input<BadgeSize>('md');
12
+ readonly class = input<string>('');
13
+
14
+ protected readonly computedClass = computed(() =>
15
+ cn(badgeVariants({ variant: this.variant(), size: this.size() }), this.class())
16
+ );
17
+ }
@@ -0,0 +1,29 @@
1
+ import { cva } from 'class-variance-authority';
2
+
3
+ export const badgeVariants = cva(
4
+ 'inline-flex items-center rounded-full border font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
5
+ {
6
+ variants: {
7
+ variant: {
8
+ default: 'border-transparent bg-primary text-primary-foreground',
9
+ secondary: 'border-transparent bg-secondary text-secondary-foreground',
10
+ outline: 'border-border text-foreground',
11
+ destructive: 'border-transparent bg-destructive text-destructive-foreground',
12
+ success: 'border-transparent bg-green-600 text-white dark:bg-green-500',
13
+ warning: 'border-transparent bg-yellow-500 text-white dark:bg-yellow-400 dark:text-black',
14
+ },
15
+ size: {
16
+ sm: 'px-2 py-0.5 text-[10px]',
17
+ md: 'px-2.5 py-0.5 text-xs',
18
+ lg: 'px-3 py-1 text-sm',
19
+ },
20
+ },
21
+ defaultVariants: {
22
+ variant: 'default',
23
+ size: 'md',
24
+ },
25
+ }
26
+ );
27
+
28
+ export type BadgeVariant = 'default' | 'secondary' | 'outline' | 'destructive' | 'success' | 'warning';
29
+ export type BadgeSize = 'sm' | 'md' | 'lg';
@@ -0,0 +1,2 @@
1
+ export { TonBadgeDirective } from './badge.directive';
2
+ export { badgeVariants, type BadgeVariant, type BadgeSize } from './badge.variants';
@@ -0,0 +1,80 @@
1
+ import { Component } from '@angular/core';
2
+ import { TestBed, ComponentFixture } from '@angular/core/testing';
3
+ import {
4
+ TonBreadcrumbDirective,
5
+ TonBreadcrumbListDirective,
6
+ TonBreadcrumbItemDirective,
7
+ TonBreadcrumbLinkDirective,
8
+ TonBreadcrumbSeparatorDirective,
9
+ TonBreadcrumbPageDirective,
10
+ } from './breadcrumb.directives';
11
+
12
+ @Component({
13
+ standalone: true,
14
+ imports: [
15
+ TonBreadcrumbDirective,
16
+ TonBreadcrumbListDirective,
17
+ TonBreadcrumbItemDirective,
18
+ TonBreadcrumbLinkDirective,
19
+ TonBreadcrumbSeparatorDirective,
20
+ TonBreadcrumbPageDirective,
21
+ ],
22
+ template: `
23
+ <nav tonBreadcrumb>
24
+ <ol tonBreadcrumbList>
25
+ <li tonBreadcrumbItem>
26
+ <a tonBreadcrumbLink href="/">Home</a>
27
+ </li>
28
+ <li tonBreadcrumbSeparator>/</li>
29
+ <li tonBreadcrumbItem>
30
+ <span tonBreadcrumbPage>Current</span>
31
+ </li>
32
+ </ol>
33
+ </nav>
34
+ `,
35
+ })
36
+ class TestHostComponent {}
37
+
38
+ describe('Breadcrumb Directives', () => {
39
+ let fixture: ComponentFixture<TestHostComponent>;
40
+
41
+ beforeEach(async () => {
42
+ await TestBed.configureTestingModule({
43
+ imports: [TestHostComponent],
44
+ }).compileComponents();
45
+
46
+ fixture = TestBed.createComponent(TestHostComponent);
47
+ fixture.detectChanges();
48
+ });
49
+
50
+ it('should set aria-label on nav', () => {
51
+ const nav = fixture.nativeElement.querySelector('nav');
52
+ expect(nav.getAttribute('aria-label')).toBe('Breadcrumb');
53
+ });
54
+
55
+ it('should apply list classes', () => {
56
+ const ol = fixture.nativeElement.querySelector('ol');
57
+ expect(ol.className).toContain('flex');
58
+ expect(ol.className).toContain('items-center');
59
+ });
60
+
61
+ it('should apply link hover classes', () => {
62
+ const link = fixture.nativeElement.querySelector('[tonBreadcrumbLink]');
63
+ expect(link.className).toContain('hover:text-foreground');
64
+ });
65
+
66
+ it('should set aria-current="page" on page', () => {
67
+ const page = fixture.nativeElement.querySelector('[tonBreadcrumbPage]');
68
+ expect(page.getAttribute('aria-current')).toBe('page');
69
+ });
70
+
71
+ it('should set aria-hidden on separator', () => {
72
+ const sep = fixture.nativeElement.querySelector('[tonBreadcrumbSeparator]');
73
+ expect(sep.getAttribute('aria-hidden')).toBe('true');
74
+ });
75
+
76
+ it('should set role="presentation" on separator', () => {
77
+ const sep = fixture.nativeElement.querySelector('[tonBreadcrumbSeparator]');
78
+ expect(sep.getAttribute('role')).toBe('presentation');
79
+ });
80
+ });
@@ -0,0 +1,78 @@
1
+ import { Directive, computed, input } from '@angular/core';
2
+ import { cn } from '../core/utils/cn';
3
+
4
+ @Directive({
5
+ selector: 'nav[tonBreadcrumb]',
6
+ host: {
7
+ '[class]': 'computedClass()',
8
+ 'aria-label': 'Breadcrumb',
9
+ },
10
+ })
11
+ export class TonBreadcrumbDirective {
12
+ readonly class = input<string>('');
13
+ protected readonly computedClass = computed(() => cn('', this.class()));
14
+ }
15
+
16
+ @Directive({
17
+ selector: 'ol[tonBreadcrumbList]',
18
+ host: { '[class]': 'computedClass()' },
19
+ })
20
+ export class TonBreadcrumbListDirective {
21
+ readonly class = input<string>('');
22
+ protected readonly computedClass = computed(() =>
23
+ cn('flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5', this.class())
24
+ );
25
+ }
26
+
27
+ @Directive({
28
+ selector: 'li[tonBreadcrumbItem]',
29
+ host: { '[class]': 'computedClass()' },
30
+ })
31
+ export class TonBreadcrumbItemDirective {
32
+ readonly class = input<string>('');
33
+ protected readonly computedClass = computed(() =>
34
+ cn('inline-flex items-center gap-1.5', this.class())
35
+ );
36
+ }
37
+
38
+ @Directive({
39
+ selector: '[tonBreadcrumbLink]',
40
+ host: { '[class]': 'computedClass()' },
41
+ })
42
+ export class TonBreadcrumbLinkDirective {
43
+ readonly class = input<string>('');
44
+ protected readonly computedClass = computed(() =>
45
+ cn('transition-colors hover:text-foreground', this.class())
46
+ );
47
+ }
48
+
49
+ @Directive({
50
+ selector: '[tonBreadcrumbSeparator]',
51
+ host: {
52
+ role: 'presentation',
53
+ '[aria-hidden]': 'true',
54
+ '[class]': 'computedClass()',
55
+ },
56
+ })
57
+ export class TonBreadcrumbSeparatorDirective {
58
+ readonly class = input<string>('');
59
+ protected readonly computedClass = computed(() =>
60
+ cn('[&>svg]:size-3.5 text-muted-foreground', this.class())
61
+ );
62
+ }
63
+
64
+ @Directive({
65
+ selector: '[tonBreadcrumbPage]',
66
+ host: {
67
+ role: 'link',
68
+ 'aria-disabled': 'true',
69
+ '[attr.aria-current]': '"page"',
70
+ '[class]': 'computedClass()',
71
+ },
72
+ })
73
+ export class TonBreadcrumbPageDirective {
74
+ readonly class = input<string>('');
75
+ protected readonly computedClass = computed(() =>
76
+ cn('font-normal text-foreground', this.class())
77
+ );
78
+ }
@@ -0,0 +1,8 @@
1
+ export {
2
+ TonBreadcrumbDirective,
3
+ TonBreadcrumbListDirective,
4
+ TonBreadcrumbItemDirective,
5
+ TonBreadcrumbLinkDirective,
6
+ TonBreadcrumbSeparatorDirective,
7
+ TonBreadcrumbPageDirective,
8
+ } from './breadcrumb.directives';
@@ -0,0 +1,92 @@
1
+ import { Component, signal } from '@angular/core';
2
+ import { TestBed, ComponentFixture } from '@angular/core/testing';
3
+ import { TonButtonDirective } from './button.directive';
4
+ import type { ButtonVariant, ButtonSize } from './button.variants';
5
+
6
+ @Component({
7
+ standalone: true,
8
+ imports: [TonButtonDirective],
9
+ template: `<button tonBtn [variant]="variant()" [size]="size()" [disabled]="disabled()" [loading]="loading()">Click</button>`,
10
+ })
11
+ class TestHostComponent {
12
+ variant = signal<ButtonVariant>('default');
13
+ size = signal<ButtonSize>('md');
14
+ disabled = signal(false);
15
+ loading = signal(false);
16
+ }
17
+
18
+ describe('TonButtonDirective', () => {
19
+ let fixture: ComponentFixture<TestHostComponent>;
20
+ let button: HTMLButtonElement;
21
+
22
+ beforeEach(async () => {
23
+ await TestBed.configureTestingModule({
24
+ imports: [TestHostComponent],
25
+ }).compileComponents();
26
+
27
+ fixture = TestBed.createComponent(TestHostComponent);
28
+ fixture.detectChanges();
29
+ button = fixture.nativeElement.querySelector('button');
30
+ });
31
+
32
+ it('should apply default variant classes', () => {
33
+ expect(button.className).toContain('bg-primary');
34
+ expect(button.className).toContain('text-primary-foreground');
35
+ });
36
+
37
+ it('should apply destructive variant', () => {
38
+ fixture.componentInstance.variant.set('destructive');
39
+ fixture.detectChanges();
40
+ expect(button.className).toContain('bg-destructive');
41
+ });
42
+
43
+ it('should apply outline variant', () => {
44
+ fixture.componentInstance.variant.set('outline');
45
+ fixture.detectChanges();
46
+ expect(button.className).toContain('border');
47
+ expect(button.className).toContain('bg-background');
48
+ });
49
+
50
+ it('should apply ghost variant', () => {
51
+ fixture.componentInstance.variant.set('ghost');
52
+ fixture.detectChanges();
53
+ expect(button.className).toContain('hover:bg-accent');
54
+ });
55
+
56
+ it('should apply sm size', () => {
57
+ fixture.componentInstance.size.set('sm');
58
+ fixture.detectChanges();
59
+ expect(button.className).toContain('h-9');
60
+ });
61
+
62
+ it('should apply lg size', () => {
63
+ fixture.componentInstance.size.set('lg');
64
+ fixture.detectChanges();
65
+ expect(button.className).toContain('h-11');
66
+ });
67
+
68
+ it('should apply icon size', () => {
69
+ fixture.componentInstance.size.set('icon');
70
+ fixture.detectChanges();
71
+ expect(button.className).toContain('w-10');
72
+ });
73
+
74
+ it('should set aria-disabled when disabled', () => {
75
+ fixture.componentInstance.disabled.set(true);
76
+ fixture.detectChanges();
77
+ expect(button.getAttribute('aria-disabled')).toBe('true');
78
+ });
79
+
80
+ it('should set aria-disabled when loading', () => {
81
+ fixture.componentInstance.loading.set(true);
82
+ fixture.detectChanges();
83
+ expect(button.getAttribute('aria-disabled')).toBe('true');
84
+ expect(button.className).toContain('cursor-wait');
85
+ });
86
+
87
+ it('should set tabindex=-1 when disabled', () => {
88
+ fixture.componentInstance.disabled.set(true);
89
+ fixture.detectChanges();
90
+ expect(button.getAttribute('tabindex')).toBe('-1');
91
+ });
92
+ });
@@ -0,0 +1,28 @@
1
+ import { Directive, computed, input } from '@angular/core';
2
+ import { cn } from '../core/utils/cn';
3
+ import { buttonVariants, type ButtonVariant, type ButtonSize } from './button.variants';
4
+
5
+ @Directive({
6
+ selector: 'button[tonBtn], a[tonBtn]',
7
+ host: {
8
+ '[class]': 'computedClass()',
9
+ '[attr.aria-disabled]': 'disabled() || loading() || null',
10
+ '[attr.disabled]': 'disabled() || loading() || null',
11
+ '[attr.tabindex]': '(disabled() || loading()) ? -1 : null',
12
+ },
13
+ })
14
+ export class TonButtonDirective {
15
+ readonly variant = input<ButtonVariant>('default');
16
+ readonly size = input<ButtonSize>('md');
17
+ readonly disabled = input(false);
18
+ readonly loading = input(false);
19
+ readonly class = input<string>('');
20
+
21
+ protected readonly computedClass = computed(() =>
22
+ cn(
23
+ buttonVariants({ variant: this.variant(), size: this.size() }),
24
+ this.loading() && 'cursor-wait opacity-70',
25
+ this.class()
26
+ )
27
+ );
28
+ }