@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.
- package/README.md +188 -0
- package/fesm2022/tony-ui-library-core.mjs +8756 -0
- package/fesm2022/tony-ui-library-core.mjs.map +1 -0
- package/package.json +55 -0
- package/schematics/collection.json +16 -0
- package/schematics/ng-add/index.d.ts +5 -0
- package/schematics/ng-add/index.js +53 -0
- package/schematics/ng-add/schema.json +19 -0
- package/schematics/ng-generate/component/index.d.ts +9 -0
- package/schematics/ng-generate/component/index.js +439 -0
- package/schematics/ng-generate/component/schema.json +32 -0
- package/src/lib/accordion/accordion.directives.spec.ts +173 -0
- package/src/lib/accordion/accordion.directives.ts +143 -0
- package/src/lib/accordion/index.ts +8 -0
- package/src/lib/alert/alert.directives.spec.ts +154 -0
- package/src/lib/alert/alert.directives.ts +67 -0
- package/src/lib/alert/alert.variants.ts +25 -0
- package/src/lib/alert/index.ts +6 -0
- package/src/lib/avatar/avatar.component.spec.ts +75 -0
- package/src/lib/avatar/avatar.component.ts +43 -0
- package/src/lib/avatar/avatar.variants.ts +26 -0
- package/src/lib/avatar/index.ts +2 -0
- package/src/lib/avatar-group/avatar-group.component.spec.ts +74 -0
- package/src/lib/avatar-group/avatar-group.component.ts +88 -0
- package/src/lib/avatar-group/index.ts +1 -0
- package/src/lib/badge/badge.directive.spec.ts +74 -0
- package/src/lib/badge/badge.directive.ts +17 -0
- package/src/lib/badge/badge.variants.ts +29 -0
- package/src/lib/badge/index.ts +2 -0
- package/src/lib/breadcrumb/breadcrumb.directives.spec.ts +80 -0
- package/src/lib/breadcrumb/breadcrumb.directives.ts +78 -0
- package/src/lib/breadcrumb/index.ts +8 -0
- package/src/lib/button/button.directive.spec.ts +92 -0
- package/src/lib/button/button.directive.ts +28 -0
- package/src/lib/button/button.variants.ts +30 -0
- package/src/lib/button/index.ts +2 -0
- package/src/lib/button-group/button-group.directive.spec.ts +46 -0
- package/src/lib/button-group/button-group.directive.ts +19 -0
- package/src/lib/button-group/button-group.variants.ts +18 -0
- package/src/lib/button-group/index.ts +2 -0
- package/src/lib/calendar/calendar.component.spec.ts +192 -0
- package/src/lib/calendar/calendar.component.ts +342 -0
- package/src/lib/calendar/calendar.types.ts +24 -0
- package/src/lib/calendar/index.ts +7 -0
- package/src/lib/card/card.directives.spec.ts +104 -0
- package/src/lib/card/card.directives.ts +72 -0
- package/src/lib/card/card.variants.ts +28 -0
- package/src/lib/card/index.ts +9 -0
- package/src/lib/carousel/carousel.directives.spec.ts +85 -0
- package/src/lib/carousel/carousel.directives.ts +159 -0
- package/src/lib/carousel/index.ts +8 -0
- package/src/lib/chat-bubble/chat-bubble.directives.spec.ts +52 -0
- package/src/lib/chat-bubble/chat-bubble.directives.ts +96 -0
- package/src/lib/chat-bubble/index.ts +11 -0
- package/src/lib/checkbox/checkbox.directive.spec.ts +57 -0
- package/src/lib/checkbox/checkbox.directive.ts +16 -0
- package/src/lib/checkbox/checkbox.variants.ts +19 -0
- package/src/lib/checkbox/index.ts +2 -0
- package/src/lib/color-picker/color-picker.component.spec.ts +328 -0
- package/src/lib/color-picker/color-picker.component.ts +537 -0
- package/src/lib/color-picker/color-picker.types.ts +24 -0
- package/src/lib/color-picker/color-picker.utils.ts +183 -0
- package/src/lib/color-picker/color-picker.variants.ts +17 -0
- package/src/lib/color-picker/index.ts +20 -0
- package/src/lib/combobox/combobox.component.spec.ts +151 -0
- package/src/lib/combobox/combobox.component.ts +264 -0
- package/src/lib/combobox/combobox.variants.ts +19 -0
- package/src/lib/combobox/index.ts +2 -0
- package/src/lib/command-palette/command-palette.component.spec.ts +178 -0
- package/src/lib/command-palette/command-palette.component.ts +194 -0
- package/src/lib/command-palette/command-palette.service.ts +36 -0
- package/src/lib/command-palette/command-palette.types.ts +23 -0
- package/src/lib/command-palette/index.ts +7 -0
- package/src/lib/data-table/data-table.component.spec.ts +443 -0
- package/src/lib/data-table/data-table.component.ts +622 -0
- package/src/lib/data-table/data-table.directives.ts +31 -0
- package/src/lib/data-table/data-table.types.ts +26 -0
- package/src/lib/data-table/index.ts +14 -0
- package/src/lib/date-picker/date-picker.component.spec.ts +131 -0
- package/src/lib/date-picker/date-picker.component.ts +220 -0
- package/src/lib/date-picker/date-picker.variants.ts +17 -0
- package/src/lib/date-picker/index.ts +2 -0
- package/src/lib/date-range-picker/date-range-picker.component.spec.ts +151 -0
- package/src/lib/date-range-picker/date-range-picker.component.ts +340 -0
- package/src/lib/date-range-picker/index.ts +1 -0
- package/src/lib/diff/diff.component.spec.ts +47 -0
- package/src/lib/diff/diff.component.ts +82 -0
- package/src/lib/diff/index.ts +1 -0
- package/src/lib/divider/divider.component.spec.ts +48 -0
- package/src/lib/divider/divider.component.ts +51 -0
- package/src/lib/divider/divider.variants.ts +22 -0
- package/src/lib/divider/index.ts +2 -0
- package/src/lib/dock/dock.directives.spec.ts +85 -0
- package/src/lib/dock/dock.directives.ts +81 -0
- package/src/lib/dock/index.ts +1 -0
- package/src/lib/drawer/drawer.directives.spec.ts +62 -0
- package/src/lib/drawer/drawer.directives.ts +80 -0
- package/src/lib/drawer/index.ts +8 -0
- package/src/lib/dropdown/dropdown.directives.spec.ts +106 -0
- package/src/lib/dropdown/dropdown.directives.ts +136 -0
- package/src/lib/dropdown/dropdown.variants.ts +27 -0
- package/src/lib/dropdown/index.ts +15 -0
- package/src/lib/fab/fab.directives.spec.ts +60 -0
- package/src/lib/fab/fab.directives.ts +77 -0
- package/src/lib/fab/index.ts +8 -0
- package/src/lib/fieldset/fieldset.directives.spec.ts +74 -0
- package/src/lib/fieldset/fieldset.directives.ts +49 -0
- package/src/lib/fieldset/fieldset.variants.ts +15 -0
- package/src/lib/fieldset/index.ts +6 -0
- package/src/lib/file-input/file-input.component.spec.ts +114 -0
- package/src/lib/file-input/file-input.component.ts +155 -0
- package/src/lib/file-input/file-input.variants.ts +25 -0
- package/src/lib/file-input/index.ts +6 -0
- package/src/lib/indicator/index.ts +6 -0
- package/src/lib/indicator/indicator.directives.spec.ts +64 -0
- package/src/lib/indicator/indicator.directives.ts +59 -0
- package/src/lib/input/index.ts +3 -0
- package/src/lib/input/input.directive.spec.ts +103 -0
- package/src/lib/input/input.directive.ts +25 -0
- package/src/lib/input/input.variants.ts +42 -0
- package/src/lib/input/label.directive.ts +16 -0
- package/src/lib/kbd/index.ts +2 -0
- package/src/lib/kbd/kbd.directive.spec.ts +42 -0
- package/src/lib/kbd/kbd.directive.ts +18 -0
- package/src/lib/kbd/kbd.variants.ts +19 -0
- package/src/lib/link/index.ts +2 -0
- package/src/lib/link/link.directive.spec.ts +41 -0
- package/src/lib/link/link.directive.ts +18 -0
- package/src/lib/link/link.variants.ts +20 -0
- package/src/lib/list/index.ts +8 -0
- package/src/lib/list/list.directives.spec.ts +65 -0
- package/src/lib/list/list.directives.ts +81 -0
- package/src/lib/loader/index.ts +2 -0
- package/src/lib/loader/loader.component.spec.ts +58 -0
- package/src/lib/loader/loader.component.ts +47 -0
- package/src/lib/loader/loader.variants.ts +21 -0
- package/src/lib/modal/dialog-ref.ts +19 -0
- package/src/lib/modal/dialog.directives.ts +84 -0
- package/src/lib/modal/dialog.service.spec.ts +52 -0
- package/src/lib/modal/dialog.service.ts +61 -0
- package/src/lib/modal/dialog.types.ts +16 -0
- package/src/lib/modal/index.ts +11 -0
- package/src/lib/navbar/index.ts +7 -0
- package/src/lib/navbar/navbar.directives.spec.ts +59 -0
- package/src/lib/navbar/navbar.directives.ts +57 -0
- package/src/lib/number-input/index.ts +2 -0
- package/src/lib/number-input/number-input.component.spec.ts +151 -0
- package/src/lib/number-input/number-input.component.ts +152 -0
- package/src/lib/number-input/number-input.variants.ts +17 -0
- package/src/lib/otp-input/index.ts +2 -0
- package/src/lib/otp-input/otp-input.component.spec.ts +252 -0
- package/src/lib/otp-input/otp-input.component.ts +274 -0
- package/src/lib/otp-input/otp-input.variants.ts +18 -0
- package/src/lib/pagination/index.ts +6 -0
- package/src/lib/pagination/pagination.component.spec.ts +59 -0
- package/src/lib/pagination/pagination.component.ts +143 -0
- package/src/lib/pagination/pagination.variants.ts +31 -0
- package/src/lib/popover/index.ts +6 -0
- package/src/lib/popover/popover.directives.spec.ts +147 -0
- package/src/lib/popover/popover.directives.ts +151 -0
- package/src/lib/progress/index.ts +7 -0
- package/src/lib/progress/progress.component.spec.ts +117 -0
- package/src/lib/progress/progress.component.ts +64 -0
- package/src/lib/progress/progress.variants.ts +43 -0
- package/src/lib/radial-progress/index.ts +5 -0
- package/src/lib/radial-progress/radial-progress.component.spec.ts +41 -0
- package/src/lib/radial-progress/radial-progress.component.ts +70 -0
- package/src/lib/radio/index.ts +2 -0
- package/src/lib/radio/radio.directive.spec.ts +46 -0
- package/src/lib/radio/radio.directive.ts +16 -0
- package/src/lib/radio/radio.variants.ts +19 -0
- package/src/lib/rating/index.ts +2 -0
- package/src/lib/rating/rating.component.spec.ts +157 -0
- package/src/lib/rating/rating.component.ts +163 -0
- package/src/lib/rating/rating.variants.ts +20 -0
- package/src/lib/select/index.ts +2 -0
- package/src/lib/select/select.component.spec.ts +112 -0
- package/src/lib/select/select.component.ts +235 -0
- package/src/lib/select/select.variants.ts +19 -0
- package/src/lib/sheet/index.ts +10 -0
- package/src/lib/sheet/sheet-ref.ts +18 -0
- package/src/lib/sheet/sheet.component.spec.ts +67 -0
- package/src/lib/sheet/sheet.directives.ts +70 -0
- package/src/lib/sheet/sheet.service.ts +100 -0
- package/src/lib/sheet/sheet.types.ts +23 -0
- package/src/lib/skeleton/index.ts +2 -0
- package/src/lib/skeleton/skeleton.directive.spec.ts +63 -0
- package/src/lib/skeleton/skeleton.directive.ts +21 -0
- package/src/lib/skeleton/skeleton.variants.ts +27 -0
- package/src/lib/slider/index.ts +2 -0
- package/src/lib/slider/slider.component.spec.ts +104 -0
- package/src/lib/slider/slider.component.ts +181 -0
- package/src/lib/slider/slider.variants.ts +25 -0
- package/src/lib/stat/index.ts +8 -0
- package/src/lib/stat/stat.directives.spec.ts +60 -0
- package/src/lib/stat/stat.directives.ts +79 -0
- package/src/lib/status/index.ts +2 -0
- package/src/lib/status/status.directive.spec.ts +43 -0
- package/src/lib/status/status.directive.ts +37 -0
- package/src/lib/status/status.variants.ts +26 -0
- package/src/lib/steps/index.ts +8 -0
- package/src/lib/steps/steps.directives.spec.ts +52 -0
- package/src/lib/steps/steps.directives.ts +78 -0
- package/src/lib/switch/index.ts +2 -0
- package/src/lib/switch/switch.component.spec.ts +98 -0
- package/src/lib/switch/switch.component.ts +76 -0
- package/src/lib/switch/switch.variants.ts +31 -0
- package/src/lib/table/index.ts +12 -0
- package/src/lib/table/table.directives.spec.ts +111 -0
- package/src/lib/table/table.directives.ts +126 -0
- package/src/lib/table/table.variants.ts +36 -0
- package/src/lib/tabs/index.ts +8 -0
- package/src/lib/tabs/tabs.directives.spec.ts +136 -0
- package/src/lib/tabs/tabs.directives.ts +126 -0
- package/src/lib/tabs/tabs.variants.ts +17 -0
- package/src/lib/tag-input/index.ts +2 -0
- package/src/lib/tag-input/tag-input.component.spec.ts +190 -0
- package/src/lib/tag-input/tag-input.component.ts +172 -0
- package/src/lib/tag-input/tag-input.variants.ts +31 -0
- package/src/lib/textarea/index.ts +7 -0
- package/src/lib/textarea/textarea.directive.spec.ts +84 -0
- package/src/lib/textarea/textarea.directive.ts +71 -0
- package/src/lib/textarea/textarea.variants.ts +34 -0
- package/src/lib/timeline/index.ts +11 -0
- package/src/lib/timeline/timeline.directives.spec.ts +55 -0
- package/src/lib/timeline/timeline.directives.ts +85 -0
- package/src/lib/toast/index.ts +3 -0
- package/src/lib/toast/toast.service.spec.ts +71 -0
- package/src/lib/toast/toast.service.ts +60 -0
- package/src/lib/toast/toast.variants.ts +38 -0
- package/src/lib/toast/toaster.component.spec.ts +38 -0
- package/src/lib/toast/toaster.component.ts +81 -0
- package/src/lib/toggle/index.ts +2 -0
- package/src/lib/toggle/toggle.directive.spec.ts +100 -0
- package/src/lib/toggle/toggle.directive.ts +61 -0
- package/src/lib/toggle/toggle.variants.ts +25 -0
- package/src/lib/tooltip/index.ts +2 -0
- package/src/lib/tooltip/tooltip.directive.spec.ts +113 -0
- package/src/lib/tooltip/tooltip.directive.ts +130 -0
- package/src/lib/tooltip/tooltip.variants.ts +20 -0
- package/src/lib/validator/index.ts +5 -0
- package/src/lib/validator/validator.directives.spec.ts +47 -0
- package/src/lib/validator/validator.directives.ts +50 -0
- package/src/styles/sonny-theme.css +171 -0
- package/types/tony-ui-library-core.d.ts +2179 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { Component, signal } from '@angular/core';
|
|
2
|
+
import { TestBed, type ComponentFixture } from '@angular/core/testing';
|
|
3
|
+
import { TonTextareaDirective } from './textarea.directive';
|
|
4
|
+
import type { TextareaVariant, TextareaSize, TextareaResize } from './textarea.variants';
|
|
5
|
+
|
|
6
|
+
@Component({
|
|
7
|
+
standalone: true,
|
|
8
|
+
imports: [TonTextareaDirective],
|
|
9
|
+
template: `
|
|
10
|
+
<textarea
|
|
11
|
+
tonTextarea
|
|
12
|
+
[variant]="variant()"
|
|
13
|
+
[textareaSize]="textareaSize()"
|
|
14
|
+
[resize]="resize()"
|
|
15
|
+
[autoResize]="autoResize()"
|
|
16
|
+
></textarea>
|
|
17
|
+
`,
|
|
18
|
+
})
|
|
19
|
+
class TestHostComponent {
|
|
20
|
+
variant = signal<TextareaVariant>('default');
|
|
21
|
+
textareaSize = signal<TextareaSize>('md');
|
|
22
|
+
resize = signal<TextareaResize>('vertical');
|
|
23
|
+
autoResize = signal(false);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe('TonTextareaDirective', () => {
|
|
27
|
+
let fixture: ComponentFixture<TestHostComponent>;
|
|
28
|
+
let el: HTMLTextAreaElement;
|
|
29
|
+
|
|
30
|
+
beforeEach(async () => {
|
|
31
|
+
await TestBed.configureTestingModule({
|
|
32
|
+
imports: [TestHostComponent],
|
|
33
|
+
}).compileComponents();
|
|
34
|
+
fixture = TestBed.createComponent(TestHostComponent);
|
|
35
|
+
fixture.detectChanges();
|
|
36
|
+
el = fixture.nativeElement.querySelector('textarea');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should apply default variant classes', () => {
|
|
40
|
+
expect(el.className).toContain('border-input');
|
|
41
|
+
expect(el.className).toContain('rounded-md');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should apply error variant classes', () => {
|
|
45
|
+
fixture.componentInstance.variant.set('error');
|
|
46
|
+
fixture.detectChanges();
|
|
47
|
+
expect(el.className).toContain('border-destructive');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should set aria-invalid for error variant', () => {
|
|
51
|
+
expect(el.getAttribute('aria-invalid')).toBeNull();
|
|
52
|
+
fixture.componentInstance.variant.set('error');
|
|
53
|
+
fixture.detectChanges();
|
|
54
|
+
expect(el.getAttribute('aria-invalid')).toBe('true');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should apply small size', () => {
|
|
58
|
+
fixture.componentInstance.textareaSize.set('sm');
|
|
59
|
+
fixture.detectChanges();
|
|
60
|
+
expect(el.className).toContain('text-xs');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should apply large size', () => {
|
|
64
|
+
fixture.componentInstance.textareaSize.set('lg');
|
|
65
|
+
fixture.detectChanges();
|
|
66
|
+
expect(el.className).toContain('text-base');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should apply resize-none when resize is "none"', () => {
|
|
70
|
+
fixture.componentInstance.resize.set('none');
|
|
71
|
+
fixture.detectChanges();
|
|
72
|
+
expect(el.className).toContain('resize-none');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should apply resize-y by default', () => {
|
|
76
|
+
expect(el.className).toContain('resize-y');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should force resize-none when autoResize is true', () => {
|
|
80
|
+
fixture.componentInstance.autoResize.set(true);
|
|
81
|
+
fixture.detectChanges();
|
|
82
|
+
expect(el.className).toContain('resize-none');
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Directive,
|
|
3
|
+
computed,
|
|
4
|
+
input,
|
|
5
|
+
effect,
|
|
6
|
+
inject,
|
|
7
|
+
ElementRef,
|
|
8
|
+
afterNextRender,
|
|
9
|
+
} from '@angular/core';
|
|
10
|
+
import { cn } from '../core/utils/cn';
|
|
11
|
+
import {
|
|
12
|
+
textareaVariants,
|
|
13
|
+
type TextareaVariant,
|
|
14
|
+
type TextareaSize,
|
|
15
|
+
type TextareaResize,
|
|
16
|
+
} from './textarea.variants';
|
|
17
|
+
|
|
18
|
+
@Directive({
|
|
19
|
+
selector: 'textarea[tonTextarea]',
|
|
20
|
+
host: {
|
|
21
|
+
'[class]': 'computedClass()',
|
|
22
|
+
'[attr.aria-invalid]': 'variant() === "error" || null',
|
|
23
|
+
'(input)': 'onInput()',
|
|
24
|
+
},
|
|
25
|
+
})
|
|
26
|
+
export class TonTextareaDirective {
|
|
27
|
+
readonly variant = input<TextareaVariant>('default');
|
|
28
|
+
readonly textareaSize = input<TextareaSize>('md');
|
|
29
|
+
readonly resize = input<TextareaResize>('vertical');
|
|
30
|
+
readonly autoResize = input(false);
|
|
31
|
+
readonly class = input<string>('');
|
|
32
|
+
|
|
33
|
+
private readonly el = inject(ElementRef<HTMLTextAreaElement>);
|
|
34
|
+
|
|
35
|
+
protected readonly computedClass = computed(() =>
|
|
36
|
+
cn(
|
|
37
|
+
textareaVariants({
|
|
38
|
+
variant: this.variant(),
|
|
39
|
+
textareaSize: this.textareaSize(),
|
|
40
|
+
resize: this.autoResize() ? 'none' : this.resize(),
|
|
41
|
+
}),
|
|
42
|
+
this.class()
|
|
43
|
+
)
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
constructor() {
|
|
47
|
+
afterNextRender(() => {
|
|
48
|
+
if (this.autoResize()) {
|
|
49
|
+
this.adjustHeight();
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
effect(() => {
|
|
54
|
+
if (this.autoResize()) {
|
|
55
|
+
this.adjustHeight();
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
protected onInput(): void {
|
|
61
|
+
if (this.autoResize()) {
|
|
62
|
+
this.adjustHeight();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private adjustHeight(): void {
|
|
67
|
+
const textarea = this.el.nativeElement;
|
|
68
|
+
textarea.style.height = 'auto';
|
|
69
|
+
textarea.style.height = `${textarea.scrollHeight}px`;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { cva } from 'class-variance-authority';
|
|
2
|
+
|
|
3
|
+
export const textareaVariants = cva(
|
|
4
|
+
'flex w-full rounded-md border bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
|
5
|
+
{
|
|
6
|
+
variants: {
|
|
7
|
+
variant: {
|
|
8
|
+
default: 'border-input',
|
|
9
|
+
error:
|
|
10
|
+
'border-destructive focus-visible:ring-destructive text-destructive placeholder:text-destructive/60',
|
|
11
|
+
},
|
|
12
|
+
textareaSize: {
|
|
13
|
+
sm: 'min-h-[60px] text-xs',
|
|
14
|
+
md: 'min-h-[80px] text-sm',
|
|
15
|
+
lg: 'min-h-[120px] text-base',
|
|
16
|
+
},
|
|
17
|
+
resize: {
|
|
18
|
+
none: 'resize-none',
|
|
19
|
+
vertical: 'resize-y',
|
|
20
|
+
horizontal: 'resize-x',
|
|
21
|
+
both: 'resize',
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
defaultVariants: {
|
|
25
|
+
variant: 'default',
|
|
26
|
+
textareaSize: 'md',
|
|
27
|
+
resize: 'vertical',
|
|
28
|
+
},
|
|
29
|
+
}
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
export type TextareaVariant = 'default' | 'error';
|
|
33
|
+
export type TextareaSize = 'sm' | 'md' | 'lg';
|
|
34
|
+
export type TextareaResize = 'none' | 'vertical' | 'horizontal' | 'both';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export {
|
|
2
|
+
TonTimelineDirective,
|
|
3
|
+
TonTimelineItemDirective,
|
|
4
|
+
TonTimelineStartDirective,
|
|
5
|
+
TonTimelineMiddleDirective,
|
|
6
|
+
TonTimelineEndDirective,
|
|
7
|
+
TON_TIMELINE,
|
|
8
|
+
type TimelineOrientation,
|
|
9
|
+
type TimelineConnect,
|
|
10
|
+
type TimelineMiddleVariant,
|
|
11
|
+
} from './timeline.directives';
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Component } from '@angular/core';
|
|
2
|
+
import { TestBed, type ComponentFixture } from '@angular/core/testing';
|
|
3
|
+
import {
|
|
4
|
+
TonTimelineDirective,
|
|
5
|
+
TonTimelineItemDirective,
|
|
6
|
+
TonTimelineStartDirective,
|
|
7
|
+
TonTimelineMiddleDirective,
|
|
8
|
+
TonTimelineEndDirective,
|
|
9
|
+
} from './timeline.directives';
|
|
10
|
+
|
|
11
|
+
@Component({
|
|
12
|
+
standalone: true,
|
|
13
|
+
imports: [TonTimelineDirective, TonTimelineItemDirective, TonTimelineStartDirective, TonTimelineMiddleDirective, TonTimelineEndDirective],
|
|
14
|
+
template: `
|
|
15
|
+
<div tonTimeline>
|
|
16
|
+
<div tonTimelineItem>
|
|
17
|
+
<div tonTimelineStart>2024</div>
|
|
18
|
+
<div tonTimelineMiddle variant="primary"></div>
|
|
19
|
+
<div tonTimelineEnd>Event 1</div>
|
|
20
|
+
</div>
|
|
21
|
+
<div tonTimelineItem>
|
|
22
|
+
<div tonTimelineStart>2025</div>
|
|
23
|
+
<div tonTimelineMiddle></div>
|
|
24
|
+
<div tonTimelineEnd>Event 2</div>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
`,
|
|
28
|
+
})
|
|
29
|
+
class TestHostComponent {}
|
|
30
|
+
|
|
31
|
+
describe('TonTimelineDirective', () => {
|
|
32
|
+
let fixture: ComponentFixture<TestHostComponent>;
|
|
33
|
+
|
|
34
|
+
beforeEach(async () => {
|
|
35
|
+
await TestBed.configureTestingModule({ imports: [TestHostComponent] }).compileComponents();
|
|
36
|
+
fixture = TestBed.createComponent(TestHostComponent);
|
|
37
|
+
fixture.detectChanges();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should render with list role', () => {
|
|
41
|
+
const timeline = fixture.nativeElement.querySelector('[tonTimeline]');
|
|
42
|
+
expect(timeline.getAttribute('role')).toBe('list');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should render items with listitem role', () => {
|
|
46
|
+
const items = fixture.nativeElement.querySelectorAll('[tonTimelineItem]');
|
|
47
|
+
expect(items.length).toBe(2);
|
|
48
|
+
expect(items[0].getAttribute('role')).toBe('listitem');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should mark middle as aria-hidden', () => {
|
|
52
|
+
const middles = fixture.nativeElement.querySelectorAll('[tonTimelineMiddle]');
|
|
53
|
+
expect(middles[0].getAttribute('aria-hidden')).toBe('true');
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { Directive, InjectionToken, computed, contentChildren, input } from '@angular/core';
|
|
2
|
+
import { cn } from '../core/utils/cn';
|
|
3
|
+
|
|
4
|
+
export const TON_TIMELINE = new InjectionToken<TonTimelineDirective>('TonTimeline');
|
|
5
|
+
export type TimelineOrientation = 'vertical' | 'horizontal';
|
|
6
|
+
export type TimelineConnect = 'start' | 'end' | 'both' | 'none';
|
|
7
|
+
export type TimelineMiddleVariant = 'default' | 'primary' | 'success' | 'error';
|
|
8
|
+
|
|
9
|
+
@Directive({
|
|
10
|
+
selector: '[tonTimelineItem]',
|
|
11
|
+
host: { 'role': 'listitem', '[class]': 'computedClass()' },
|
|
12
|
+
})
|
|
13
|
+
export class TonTimelineItemDirective {
|
|
14
|
+
readonly connect = input<TimelineConnect>('both');
|
|
15
|
+
readonly class = input<string>('');
|
|
16
|
+
protected readonly computedClass = computed(() =>
|
|
17
|
+
cn('relative flex gap-4', this.class())
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@Directive({
|
|
22
|
+
selector: '[tonTimelineStart]',
|
|
23
|
+
host: { '[class]': 'computedClass()' },
|
|
24
|
+
})
|
|
25
|
+
export class TonTimelineStartDirective {
|
|
26
|
+
readonly class = input<string>('');
|
|
27
|
+
protected readonly computedClass = computed(() =>
|
|
28
|
+
cn('text-sm text-muted-foreground min-w-[80px] text-right', this.class())
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@Directive({
|
|
33
|
+
selector: '[tonTimelineMiddle]',
|
|
34
|
+
host: { '[class]': 'computedClass()', 'aria-hidden': 'true' },
|
|
35
|
+
})
|
|
36
|
+
export class TonTimelineMiddleDirective {
|
|
37
|
+
readonly variant = input<TimelineMiddleVariant>('default');
|
|
38
|
+
readonly class = input<string>('');
|
|
39
|
+
protected readonly computedClass = computed(() => {
|
|
40
|
+
const v = this.variant();
|
|
41
|
+
const variantClass =
|
|
42
|
+
v === 'primary' ? 'bg-primary' :
|
|
43
|
+
v === 'success' ? 'bg-green-600 dark:bg-green-500' :
|
|
44
|
+
v === 'error' ? 'bg-destructive' :
|
|
45
|
+
'bg-border';
|
|
46
|
+
return cn('flex flex-col items-center', variantClass, this.class());
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@Directive({
|
|
51
|
+
selector: '[tonTimelineEnd]',
|
|
52
|
+
host: { '[class]': 'computedClass()' },
|
|
53
|
+
})
|
|
54
|
+
export class TonTimelineEndDirective {
|
|
55
|
+
readonly class = input<string>('');
|
|
56
|
+
protected readonly computedClass = computed(() =>
|
|
57
|
+
cn('flex-1 pb-8', this.class())
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
@Directive({
|
|
62
|
+
selector: '[tonTimeline]',
|
|
63
|
+
exportAs: 'tonTimeline',
|
|
64
|
+
providers: [{ provide: TON_TIMELINE, useExisting: TonTimelineDirective }],
|
|
65
|
+
host: {
|
|
66
|
+
'role': 'list',
|
|
67
|
+
'aria-label': 'Timeline',
|
|
68
|
+
'[class]': 'computedClass()',
|
|
69
|
+
},
|
|
70
|
+
})
|
|
71
|
+
export class TonTimelineDirective {
|
|
72
|
+
readonly orientation = input<TimelineOrientation>('vertical');
|
|
73
|
+
readonly class = input<string>('');
|
|
74
|
+
|
|
75
|
+
readonly items = contentChildren(TonTimelineItemDirective);
|
|
76
|
+
|
|
77
|
+
protected readonly computedClass = computed(() => {
|
|
78
|
+
const o = this.orientation();
|
|
79
|
+
return cn(
|
|
80
|
+
'relative',
|
|
81
|
+
o === 'vertical' ? 'flex flex-col' : 'flex flex-row',
|
|
82
|
+
this.class()
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { TestBed } from '@angular/core/testing';
|
|
2
|
+
import { TonToastService } from './toast.service';
|
|
3
|
+
|
|
4
|
+
describe('TonToastService', () => {
|
|
5
|
+
let service: TonToastService;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
TestBed.configureTestingModule({});
|
|
9
|
+
service = TestBed.inject(TonToastService);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('should create a toast', () => {
|
|
13
|
+
const id = service.show({ title: 'Test', duration: 0 });
|
|
14
|
+
expect(id).toBeTruthy();
|
|
15
|
+
expect(service.toasts().length).toBe(1);
|
|
16
|
+
expect(service.toasts()[0].title).toBe('Test');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should create toast with variant', () => {
|
|
20
|
+
service.show({ title: 'Error', variant: 'destructive', duration: 0 });
|
|
21
|
+
expect(service.toasts()[0].variant).toBe('destructive');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should dismiss a toast', () => {
|
|
25
|
+
const id = service.show({ title: 'Test', duration: 0 });
|
|
26
|
+
service.dismiss(id);
|
|
27
|
+
expect(service.toasts().length).toBe(0);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should dismiss all toasts', () => {
|
|
31
|
+
service.show({ title: 'Test 1', duration: 0 });
|
|
32
|
+
service.show({ title: 'Test 2', duration: 0 });
|
|
33
|
+
expect(service.toasts().length).toBe(2);
|
|
34
|
+
service.dismissAll();
|
|
35
|
+
expect(service.toasts().length).toBe(0);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should auto-dismiss after duration', async () => {
|
|
39
|
+
service.show({ title: 'Test', duration: 50 });
|
|
40
|
+
expect(service.toasts().length).toBe(1);
|
|
41
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
42
|
+
expect(service.toasts().length).toBe(0);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should provide success shortcut', () => {
|
|
46
|
+
service.success('Done', 'Task completed');
|
|
47
|
+
const toast = service.toasts()[0];
|
|
48
|
+
expect(toast.variant).toBe('success');
|
|
49
|
+
expect(toast.description).toBe('Task completed');
|
|
50
|
+
service.dismiss(toast.id);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should provide error shortcut', () => {
|
|
54
|
+
service.error('Failed');
|
|
55
|
+
expect(service.toasts()[0].variant).toBe('destructive');
|
|
56
|
+
service.dismissAll();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should provide warning shortcut', () => {
|
|
60
|
+
service.warning('Caution');
|
|
61
|
+
expect(service.toasts()[0].variant).toBe('warning');
|
|
62
|
+
service.dismissAll();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should track count', () => {
|
|
66
|
+
expect(service.count()).toBe(0);
|
|
67
|
+
service.show({ title: 'A', duration: 0 });
|
|
68
|
+
service.show({ title: 'B', duration: 0 });
|
|
69
|
+
expect(service.count()).toBe(2);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Injectable, signal, computed } from '@angular/core';
|
|
2
|
+
import type { ToastConfig, ToastData } from './toast.variants';
|
|
3
|
+
|
|
4
|
+
@Injectable({ providedIn: 'root' })
|
|
5
|
+
export class TonToastService {
|
|
6
|
+
private readonly _toasts = signal<ToastData[]>([]);
|
|
7
|
+
private readonly _timers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
8
|
+
private _idCounter = 0;
|
|
9
|
+
|
|
10
|
+
readonly toasts = this._toasts.asReadonly();
|
|
11
|
+
readonly count = computed(() => this._toasts().length);
|
|
12
|
+
|
|
13
|
+
show(config: ToastConfig): string {
|
|
14
|
+
const id = `ton-toast-${++this._idCounter}`;
|
|
15
|
+
const toast: ToastData = {
|
|
16
|
+
id,
|
|
17
|
+
variant: 'default',
|
|
18
|
+
duration: 5000,
|
|
19
|
+
...config,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
this._toasts.update(toasts => [...toasts, toast]);
|
|
23
|
+
|
|
24
|
+
if (toast.duration && toast.duration > 0) {
|
|
25
|
+
const timer = setTimeout(() => this.dismiss(id), toast.duration);
|
|
26
|
+
this._timers.set(id, timer);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return id;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
dismiss(id: string): void {
|
|
33
|
+
const timer = this._timers.get(id);
|
|
34
|
+
if (timer) {
|
|
35
|
+
clearTimeout(timer);
|
|
36
|
+
this._timers.delete(id);
|
|
37
|
+
}
|
|
38
|
+
this._toasts.update(toasts => toasts.filter(t => t.id !== id));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
dismissAll(): void {
|
|
42
|
+
for (const timer of this._timers.values()) {
|
|
43
|
+
clearTimeout(timer);
|
|
44
|
+
}
|
|
45
|
+
this._timers.clear();
|
|
46
|
+
this._toasts.set([]);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
success(title: string, description?: string): string {
|
|
50
|
+
return this.show({ title, description, variant: 'success' });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
error(title: string, description?: string): string {
|
|
54
|
+
return this.show({ title, description, variant: 'destructive' });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
warning(title: string, description?: string): string {
|
|
58
|
+
return this.show({ title, description, variant: 'warning' });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { cva } from 'class-variance-authority';
|
|
2
|
+
|
|
3
|
+
export const toastVariants = cva(
|
|
4
|
+
'group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-sm border p-6 pr-8 shadow-lg transition-all',
|
|
5
|
+
{
|
|
6
|
+
variants: {
|
|
7
|
+
variant: {
|
|
8
|
+
default: 'bg-background border-border text-foreground',
|
|
9
|
+
destructive: 'bg-destructive border-destructive text-destructive-foreground',
|
|
10
|
+
success: 'bg-green-600 border-green-600 text-white',
|
|
11
|
+
warning: 'bg-yellow-500 border-yellow-500 text-white',
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
defaultVariants: {
|
|
15
|
+
variant: 'default',
|
|
16
|
+
},
|
|
17
|
+
}
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
export type ToastVariant = 'default' | 'destructive' | 'success' | 'warning';
|
|
21
|
+
export type ToastPosition = 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left' | 'top-center' | 'bottom-center';
|
|
22
|
+
|
|
23
|
+
export interface ToastAction {
|
|
24
|
+
label: string;
|
|
25
|
+
onClick: () => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ToastConfig {
|
|
29
|
+
title: string;
|
|
30
|
+
description?: string;
|
|
31
|
+
variant?: ToastVariant;
|
|
32
|
+
duration?: number;
|
|
33
|
+
action?: ToastAction;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface ToastData extends ToastConfig {
|
|
37
|
+
id: string;
|
|
38
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Component } from '@angular/core';
|
|
2
|
+
import { TestBed, type ComponentFixture } from '@angular/core/testing';
|
|
3
|
+
import { TonToasterComponent } from './toaster.component';
|
|
4
|
+
import { TonToastService } from './toast.service';
|
|
5
|
+
|
|
6
|
+
@Component({
|
|
7
|
+
standalone: true,
|
|
8
|
+
imports: [TonToasterComponent],
|
|
9
|
+
template: `<ton-toaster />`,
|
|
10
|
+
})
|
|
11
|
+
class TestHostComponent {}
|
|
12
|
+
|
|
13
|
+
describe('TonToasterComponent', () => {
|
|
14
|
+
let fixture: ComponentFixture<TestHostComponent>;
|
|
15
|
+
let service: TonToastService;
|
|
16
|
+
|
|
17
|
+
beforeEach(async () => {
|
|
18
|
+
await TestBed.configureTestingModule({ imports: [TestHostComponent] }).compileComponents();
|
|
19
|
+
fixture = TestBed.createComponent(TestHostComponent);
|
|
20
|
+
service = TestBed.inject(TonToastService);
|
|
21
|
+
fixture.detectChanges();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should render region with aria-label', () => {
|
|
25
|
+
const region = fixture.nativeElement.querySelector('[role="region"]');
|
|
26
|
+
expect(region).toBeTruthy();
|
|
27
|
+
expect(region.getAttribute('aria-label')).toBe('Notifications');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should render toast items with aria-atomic="true"', () => {
|
|
31
|
+
service.show({ title: 'Test toast' });
|
|
32
|
+
fixture.detectChanges();
|
|
33
|
+
const alert = fixture.nativeElement.querySelector('[role="alert"]');
|
|
34
|
+
expect(alert).toBeTruthy();
|
|
35
|
+
expect(alert.getAttribute('aria-atomic')).toBe('true');
|
|
36
|
+
expect(alert.getAttribute('aria-live')).toBe('polite');
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { ChangeDetectionStrategy, Component, inject, input, computed } from '@angular/core';
|
|
2
|
+
import { TonToastService } from './toast.service';
|
|
3
|
+
import { toastVariants, type ToastPosition, type ToastVariant } from './toast.variants';
|
|
4
|
+
import { cn } from '../core/utils/cn';
|
|
5
|
+
|
|
6
|
+
@Component({
|
|
7
|
+
selector: 'ton-toaster',
|
|
8
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
9
|
+
template: `
|
|
10
|
+
<div [class]="containerClass()" role="region" aria-label="Notifications" tabindex="-1">
|
|
11
|
+
@for (toast of visibleToasts(); track toast.id) {
|
|
12
|
+
<div
|
|
13
|
+
[class]="toastClasses[toast.variant ?? 'default']"
|
|
14
|
+
role="alert"
|
|
15
|
+
aria-live="polite"
|
|
16
|
+
aria-atomic="true"
|
|
17
|
+
>
|
|
18
|
+
<div class="grid gap-1">
|
|
19
|
+
<div class="text-sm font-semibold">{{ toast.title }}</div>
|
|
20
|
+
@if (toast.description) {
|
|
21
|
+
<div class="text-sm opacity-90">{{ toast.description }}</div>
|
|
22
|
+
}
|
|
23
|
+
</div>
|
|
24
|
+
<div class="flex items-center gap-2">
|
|
25
|
+
@if (toast.action) {
|
|
26
|
+
<button
|
|
27
|
+
class="inline-flex h-8 shrink-0 items-center justify-center rounded-sm border bg-transparent px-3 text-sm font-medium transition-colors hover:bg-secondary"
|
|
28
|
+
(click)="toast.action!.onClick()"
|
|
29
|
+
>
|
|
30
|
+
{{ toast.action!.label }}
|
|
31
|
+
</button>
|
|
32
|
+
}
|
|
33
|
+
<button
|
|
34
|
+
class="absolute right-2 top-2 rounded-sm p-1 opacity-0 transition-opacity hover:opacity-100 group-hover:opacity-100 focus:opacity-100"
|
|
35
|
+
aria-label="Close"
|
|
36
|
+
(click)="dismiss(toast.id)"
|
|
37
|
+
>
|
|
38
|
+
<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"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
|
|
39
|
+
</button>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
}
|
|
43
|
+
</div>
|
|
44
|
+
`,
|
|
45
|
+
})
|
|
46
|
+
export class TonToasterComponent {
|
|
47
|
+
private readonly toastService = inject(TonToastService);
|
|
48
|
+
|
|
49
|
+
readonly position = input<ToastPosition>('bottom-right');
|
|
50
|
+
readonly maxToasts = input(5);
|
|
51
|
+
|
|
52
|
+
readonly visibleToasts = computed(() =>
|
|
53
|
+
this.toastService.toasts().slice(-this.maxToasts())
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
readonly containerClass = computed(() => {
|
|
57
|
+
const pos = this.position();
|
|
58
|
+
const base = 'fixed z-[100] flex max-h-screen w-full flex-col-reverse gap-2 p-4 sm:max-w-[420px]';
|
|
59
|
+
const posMap: Record<ToastPosition, string> = {
|
|
60
|
+
'top-right': 'top-0 right-0',
|
|
61
|
+
'top-left': 'top-0 left-0',
|
|
62
|
+
'bottom-right': 'bottom-0 right-0',
|
|
63
|
+
'bottom-left': 'bottom-0 left-0',
|
|
64
|
+
'top-center': 'top-0 left-1/2 -translate-x-1/2',
|
|
65
|
+
'bottom-center': 'bottom-0 left-1/2 -translate-x-1/2',
|
|
66
|
+
};
|
|
67
|
+
return cn(base, posMap[pos]);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
/** Pre-computed toast classes by variant — avoids method calls in the template. */
|
|
71
|
+
readonly toastClasses: Record<ToastVariant, string> = {
|
|
72
|
+
default: cn(toastVariants({ variant: 'default' })),
|
|
73
|
+
destructive: cn(toastVariants({ variant: 'destructive' })),
|
|
74
|
+
success: cn(toastVariants({ variant: 'success' })),
|
|
75
|
+
warning: cn(toastVariants({ variant: 'warning' })),
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
dismiss(id: string): void {
|
|
79
|
+
this.toastService.dismiss(id);
|
|
80
|
+
}
|
|
81
|
+
}
|