@sonny-ui/core 0.1.0-alpha.1 → 0.1.0-alpha.11
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 +101 -32
- package/fesm2022/sonny-ui-core.mjs +3031 -42
- package/fesm2022/sonny-ui-core.mjs.map +1 -1
- package/package.json +8 -5
- package/schematics/ng-add/schema.json +1 -1
- package/schematics/ng-generate/component/index.js +182 -1
- package/schematics/ng-generate/component/schema.json +2 -2
- package/src/lib/accordion/accordion.directives.spec.ts +173 -0
- package/src/lib/accordion/accordion.directives.ts +147 -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 +70 -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 +44 -0
- package/src/lib/avatar/avatar.variants.ts +26 -0
- package/src/lib/avatar/index.ts +2 -0
- package/src/lib/badge/badge.directive.spec.ts +74 -0
- package/src/lib/badge/badge.directive.ts +18 -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 +84 -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 +29 -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 +20 -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 +105 -0
- package/src/lib/calendar/calendar.component.ts +231 -0
- package/src/lib/calendar/index.ts +1 -0
- package/src/lib/card/card.directives.spec.ts +104 -0
- package/src/lib/card/card.directives.ts +78 -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 +164 -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 +102 -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 +17 -0
- package/src/lib/checkbox/checkbox.variants.ts +19 -0
- package/src/lib/checkbox/index.ts +2 -0
- package/src/lib/combobox/combobox.component.spec.ts +151 -0
- package/src/lib/combobox/combobox.component.ts +279 -0
- package/src/lib/combobox/combobox.variants.ts +19 -0
- package/src/lib/combobox/index.ts +2 -0
- package/src/lib/diff/diff.component.spec.ts +47 -0
- package/src/lib/diff/diff.component.ts +83 -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 +52 -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 +83 -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 +83 -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 +143 -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 +80 -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 +52 -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 +168 -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 +61 -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 +26 -0
- package/src/lib/input/input.variants.ts +42 -0
- package/src/lib/input/label.directive.ts +17 -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 +19 -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 +19 -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 +86 -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 +48 -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 +90 -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 +61 -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 +144 -0
- package/src/lib/pagination/pagination.variants.ts +31 -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 +65 -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 +71 -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 +17 -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 +171 -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 +250 -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 +75 -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 +22 -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 +188 -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 +84 -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 +38 -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 +80 -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 +84 -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 +134 -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 +130 -0
- package/src/lib/tabs/tabs.variants.ts +17 -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 +72 -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 +90 -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 +82 -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 +73 -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 +131 -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 +52 -0
- package/types/sonny-ui-core.d.ts +878 -11
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ChangeDetectionStrategy,
|
|
3
|
+
Component,
|
|
4
|
+
computed,
|
|
5
|
+
effect,
|
|
6
|
+
forwardRef,
|
|
7
|
+
input,
|
|
8
|
+
model,
|
|
9
|
+
output,
|
|
10
|
+
signal,
|
|
11
|
+
viewChild,
|
|
12
|
+
ElementRef,
|
|
13
|
+
} from '@angular/core';
|
|
14
|
+
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
|
15
|
+
import { cn } from '../core/utils/cn';
|
|
16
|
+
import {
|
|
17
|
+
fileInputVariants,
|
|
18
|
+
type FileInputVariant,
|
|
19
|
+
type FileInputSize,
|
|
20
|
+
} from './file-input.variants';
|
|
21
|
+
|
|
22
|
+
@Component({
|
|
23
|
+
selector: 'sny-file-input',
|
|
24
|
+
standalone: true,
|
|
25
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
26
|
+
host: {
|
|
27
|
+
'[class]': '"w-full"',
|
|
28
|
+
},
|
|
29
|
+
providers: [
|
|
30
|
+
{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => SnyFileInputComponent), multi: true },
|
|
31
|
+
],
|
|
32
|
+
template: `
|
|
33
|
+
<label
|
|
34
|
+
[class]="labelClass()"
|
|
35
|
+
(dragover)="onDragOver($event)"
|
|
36
|
+
(dragleave)="onDragLeave($event)"
|
|
37
|
+
(drop)="onDrop($event)"
|
|
38
|
+
>
|
|
39
|
+
<input
|
|
40
|
+
#fileInput
|
|
41
|
+
type="file"
|
|
42
|
+
class="sr-only"
|
|
43
|
+
[accept]="accept()"
|
|
44
|
+
[multiple]="multiple()"
|
|
45
|
+
[disabled]="isDisabled()"
|
|
46
|
+
[attr.aria-label]="placeholder()"
|
|
47
|
+
[attr.aria-invalid]="variant() === 'error' || null"
|
|
48
|
+
(change)="onFileSelected($event)"
|
|
49
|
+
(blur)="onTouched()"
|
|
50
|
+
/>
|
|
51
|
+
<span class="truncate text-muted-foreground">{{ fileName() }}</span>
|
|
52
|
+
</label>
|
|
53
|
+
`,
|
|
54
|
+
})
|
|
55
|
+
export class SnyFileInputComponent implements ControlValueAccessor {
|
|
56
|
+
readonly accept = input('');
|
|
57
|
+
readonly multiple = input(false);
|
|
58
|
+
readonly disabled = input(false);
|
|
59
|
+
readonly placeholder = input('Choose file...');
|
|
60
|
+
readonly variant = input<FileInputVariant>('default');
|
|
61
|
+
readonly size = input<FileInputSize>('md');
|
|
62
|
+
readonly maxSize = input(0);
|
|
63
|
+
readonly class = input<string>('');
|
|
64
|
+
|
|
65
|
+
readonly value = model<FileList | null>(null);
|
|
66
|
+
readonly fileChange = output<FileList>();
|
|
67
|
+
readonly error = output<string>();
|
|
68
|
+
|
|
69
|
+
private readonly _disabledByCva = signal(false);
|
|
70
|
+
protected readonly isDisabled = computed(() => this.disabled() || this._disabledByCva());
|
|
71
|
+
|
|
72
|
+
private readonly fileInputRef = viewChild<ElementRef<HTMLInputElement>>('fileInput');
|
|
73
|
+
readonly isDragOver = signal(false);
|
|
74
|
+
|
|
75
|
+
private _onChange: (value: FileList | null) => void = () => {};
|
|
76
|
+
protected onTouched: () => void = () => {};
|
|
77
|
+
private _writing = false;
|
|
78
|
+
|
|
79
|
+
constructor() {
|
|
80
|
+
effect(() => {
|
|
81
|
+
const val = this.value();
|
|
82
|
+
if (this._writing) {
|
|
83
|
+
this._writing = false;
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
this._onChange(val);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
writeValue(val: FileList | null): void {
|
|
91
|
+
this._writing = true;
|
|
92
|
+
this.value.set(val ?? null);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
registerOnChange(fn: (value: FileList | null) => void): void {
|
|
96
|
+
this._onChange = fn;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
registerOnTouched(fn: () => void): void {
|
|
100
|
+
this.onTouched = fn;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
setDisabledState(isDisabled: boolean): void {
|
|
104
|
+
this._disabledByCva.set(isDisabled);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
readonly fileName = computed(() => {
|
|
108
|
+
const files = this.value();
|
|
109
|
+
if (!files || files.length === 0) return this.placeholder();
|
|
110
|
+
if (files.length === 1) return files[0].name;
|
|
111
|
+
return `${files.length} files selected`;
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
protected readonly labelClass = computed(() =>
|
|
115
|
+
cn(
|
|
116
|
+
fileInputVariants({ variant: this.variant(), size: this.size() }),
|
|
117
|
+
this.isDragOver() && 'ring-2 ring-ring',
|
|
118
|
+
this.class()
|
|
119
|
+
)
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
onFileSelected(event: Event): void {
|
|
123
|
+
const input = event.target as HTMLInputElement;
|
|
124
|
+
if (input.files) {
|
|
125
|
+
this.processFiles(input.files);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
onDragOver(event: DragEvent): void {
|
|
130
|
+
event.preventDefault();
|
|
131
|
+
if (!this.isDisabled()) {
|
|
132
|
+
this.isDragOver.set(true);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
onDragLeave(event: DragEvent): void {
|
|
137
|
+
event.preventDefault();
|
|
138
|
+
this.isDragOver.set(false);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
onDrop(event: DragEvent): void {
|
|
142
|
+
event.preventDefault();
|
|
143
|
+
this.isDragOver.set(false);
|
|
144
|
+
if (!this.isDisabled() && event.dataTransfer?.files) {
|
|
145
|
+
this.processFiles(event.dataTransfer.files);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
clear(): void {
|
|
150
|
+
this.value.set(null);
|
|
151
|
+
const inputEl = this.fileInputRef()?.nativeElement;
|
|
152
|
+
if (inputEl) inputEl.value = '';
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private processFiles(files: FileList): void {
|
|
156
|
+
const maxSize = this.maxSize();
|
|
157
|
+
if (maxSize > 0) {
|
|
158
|
+
for (let i = 0; i < files.length; i++) {
|
|
159
|
+
if (files[i].size > maxSize) {
|
|
160
|
+
this.error.emit(`File "${files[i].name}" exceeds maximum size of ${maxSize} bytes`);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
this.value.set(files);
|
|
166
|
+
this.fileChange.emit(files);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { cva } from 'class-variance-authority';
|
|
2
|
+
|
|
3
|
+
export const fileInputVariants = cva(
|
|
4
|
+
'flex w-full cursor-pointer items-center gap-2 rounded-md border bg-background px-3 py-2 text-sm ring-offset-background transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium 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: 'border-destructive focus-visible:ring-destructive',
|
|
10
|
+
},
|
|
11
|
+
size: {
|
|
12
|
+
sm: 'h-8 text-xs',
|
|
13
|
+
md: 'h-10 text-sm',
|
|
14
|
+
lg: 'h-12 text-base',
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
defaultVariants: {
|
|
18
|
+
variant: 'default',
|
|
19
|
+
size: 'md',
|
|
20
|
+
},
|
|
21
|
+
}
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
export type FileInputVariant = 'default' | 'error';
|
|
25
|
+
export type FileInputSize = 'sm' | 'md' | 'lg';
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Component, signal } from '@angular/core';
|
|
2
|
+
import { TestBed, type ComponentFixture } from '@angular/core/testing';
|
|
3
|
+
import { SnyIndicatorDirective, SnyIndicatorBadgeDirective, type IndicatorPosition, type IndicatorVariant } from './indicator.directives';
|
|
4
|
+
|
|
5
|
+
@Component({
|
|
6
|
+
standalone: true,
|
|
7
|
+
imports: [SnyIndicatorDirective, SnyIndicatorBadgeDirective],
|
|
8
|
+
template: `
|
|
9
|
+
<div snyIndicator>
|
|
10
|
+
<span snyIndicatorBadge [position]="position()" [variant]="variant()" [ariaLabel]="badgeLabel()">5</span>
|
|
11
|
+
<div>Content</div>
|
|
12
|
+
</div>
|
|
13
|
+
`,
|
|
14
|
+
})
|
|
15
|
+
class TestHostComponent {
|
|
16
|
+
position = signal<IndicatorPosition>('top-end');
|
|
17
|
+
variant = signal<IndicatorVariant>('default');
|
|
18
|
+
badgeLabel = signal('');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe('SnyIndicatorDirective', () => {
|
|
22
|
+
let fixture: ComponentFixture<TestHostComponent>;
|
|
23
|
+
|
|
24
|
+
beforeEach(async () => {
|
|
25
|
+
await TestBed.configureTestingModule({ imports: [TestHostComponent] }).compileComponents();
|
|
26
|
+
fixture = TestBed.createComponent(TestHostComponent);
|
|
27
|
+
fixture.detectChanges();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should render indicator container', () => {
|
|
31
|
+
const el = fixture.nativeElement.querySelector('[snyIndicator]');
|
|
32
|
+
expect(el.className).toContain('relative');
|
|
33
|
+
expect(el.className).toContain('inline-flex');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should position badge at top-end by default', () => {
|
|
37
|
+
const badge = fixture.nativeElement.querySelector('[snyIndicatorBadge]');
|
|
38
|
+
expect(badge.className).toContain('top-0');
|
|
39
|
+
expect(badge.className).toContain('right-0');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should change position', () => {
|
|
43
|
+
fixture.componentInstance.position.set('bottom-start');
|
|
44
|
+
fixture.detectChanges();
|
|
45
|
+
const badge = fixture.nativeElement.querySelector('[snyIndicatorBadge]');
|
|
46
|
+
expect(badge.className).toContain('bottom-0');
|
|
47
|
+
expect(badge.className).toContain('left-0');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should apply error variant', () => {
|
|
51
|
+
fixture.componentInstance.variant.set('error');
|
|
52
|
+
fixture.detectChanges();
|
|
53
|
+
const badge = fixture.nativeElement.querySelector('[snyIndicatorBadge]');
|
|
54
|
+
expect(badge.className).toContain('bg-destructive');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should set aria-label on badge when ariaLabel input is provided', () => {
|
|
58
|
+
const badge = fixture.nativeElement.querySelector('[snyIndicatorBadge]');
|
|
59
|
+
expect(badge.getAttribute('aria-label')).toBeNull();
|
|
60
|
+
fixture.componentInstance.badgeLabel.set('5 notifications');
|
|
61
|
+
fixture.detectChanges();
|
|
62
|
+
expect(badge.getAttribute('aria-label')).toBe('5 notifications');
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Directive, computed, input } from '@angular/core';
|
|
2
|
+
import { cn } from '../core/utils/cn';
|
|
3
|
+
|
|
4
|
+
export type IndicatorPosition = 'top-start' | 'top-center' | 'top-end' | 'middle-start' | 'middle-end' | 'bottom-start' | 'bottom-center' | 'bottom-end';
|
|
5
|
+
export type IndicatorVariant = 'default' | 'primary' | 'secondary' | 'success' | 'warning' | 'error';
|
|
6
|
+
|
|
7
|
+
const positionMap: Record<IndicatorPosition, string> = {
|
|
8
|
+
'top-start': 'top-0 left-0 -translate-x-1/2 -translate-y-1/2',
|
|
9
|
+
'top-center': 'top-0 left-1/2 -translate-x-1/2 -translate-y-1/2',
|
|
10
|
+
'top-end': 'top-0 right-0 translate-x-1/2 -translate-y-1/2',
|
|
11
|
+
'middle-start': 'top-1/2 left-0 -translate-x-1/2 -translate-y-1/2',
|
|
12
|
+
'middle-end': 'top-1/2 right-0 translate-x-1/2 -translate-y-1/2',
|
|
13
|
+
'bottom-start': 'bottom-0 left-0 -translate-x-1/2 translate-y-1/2',
|
|
14
|
+
'bottom-center': 'bottom-0 left-1/2 -translate-x-1/2 translate-y-1/2',
|
|
15
|
+
'bottom-end': 'bottom-0 right-0 translate-x-1/2 translate-y-1/2',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const variantMap: Record<IndicatorVariant, string> = {
|
|
19
|
+
default: 'bg-primary text-primary-foreground',
|
|
20
|
+
primary: 'bg-primary text-primary-foreground',
|
|
21
|
+
secondary: 'bg-secondary text-secondary-foreground',
|
|
22
|
+
success: 'bg-green-600 text-white dark:bg-green-500',
|
|
23
|
+
warning: 'bg-yellow-500 text-white dark:bg-yellow-400 dark:text-black',
|
|
24
|
+
error: 'bg-destructive text-destructive-foreground',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
@Directive({
|
|
28
|
+
selector: '[snyIndicator]',
|
|
29
|
+
standalone: true,
|
|
30
|
+
host: { '[class]': 'computedClass()' },
|
|
31
|
+
})
|
|
32
|
+
export class SnyIndicatorDirective {
|
|
33
|
+
readonly class = input<string>('');
|
|
34
|
+
protected readonly computedClass = computed(() =>
|
|
35
|
+
cn('relative inline-flex', this.class())
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@Directive({
|
|
40
|
+
selector: '[snyIndicatorBadge]',
|
|
41
|
+
standalone: true,
|
|
42
|
+
host: {
|
|
43
|
+
'[class]': 'computedClass()',
|
|
44
|
+
'[attr.aria-label]': 'ariaLabel() || null',
|
|
45
|
+
},
|
|
46
|
+
})
|
|
47
|
+
export class SnyIndicatorBadgeDirective {
|
|
48
|
+
readonly position = input<IndicatorPosition>('top-end');
|
|
49
|
+
readonly variant = input<IndicatorVariant>('default');
|
|
50
|
+
readonly ariaLabel = input<string>('');
|
|
51
|
+
readonly class = input<string>('');
|
|
52
|
+
|
|
53
|
+
protected readonly computedClass = computed(() =>
|
|
54
|
+
cn(
|
|
55
|
+
'absolute z-10 inline-flex items-center justify-center rounded-full text-xs font-bold min-w-[1.25rem] h-5 px-1',
|
|
56
|
+
positionMap[this.position()],
|
|
57
|
+
variantMap[this.variant()],
|
|
58
|
+
this.class()
|
|
59
|
+
)
|
|
60
|
+
);
|
|
61
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { Component, signal } from '@angular/core';
|
|
2
|
+
import { TestBed, ComponentFixture } from '@angular/core/testing';
|
|
3
|
+
import { SnyInputDirective } from './input.directive';
|
|
4
|
+
import { SnyLabelDirective } from './label.directive';
|
|
5
|
+
import type { InputVariant, InputSize } from './input.variants';
|
|
6
|
+
|
|
7
|
+
@Component({
|
|
8
|
+
standalone: true,
|
|
9
|
+
imports: [SnyInputDirective, SnyLabelDirective],
|
|
10
|
+
template: `
|
|
11
|
+
<label snyLabel [variant]="variant()">Name</label>
|
|
12
|
+
<input snyInput [variant]="variant()" [inputSize]="inputSize()" [ariaDescribedBy]="describedBy()" />
|
|
13
|
+
`,
|
|
14
|
+
})
|
|
15
|
+
class TestHostComponent {
|
|
16
|
+
variant = signal<InputVariant>('default');
|
|
17
|
+
inputSize = signal<InputSize>('md');
|
|
18
|
+
describedBy = signal('');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe('SnyInputDirective', () => {
|
|
22
|
+
let fixture: ComponentFixture<TestHostComponent>;
|
|
23
|
+
let input: HTMLInputElement;
|
|
24
|
+
|
|
25
|
+
beforeEach(async () => {
|
|
26
|
+
await TestBed.configureTestingModule({
|
|
27
|
+
imports: [TestHostComponent],
|
|
28
|
+
}).compileComponents();
|
|
29
|
+
fixture = TestBed.createComponent(TestHostComponent);
|
|
30
|
+
fixture.detectChanges();
|
|
31
|
+
input = fixture.nativeElement.querySelector('input');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should apply default input classes', () => {
|
|
35
|
+
expect(input.className).toContain('border-input');
|
|
36
|
+
expect(input.className).toContain('h-10');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should apply error variant', () => {
|
|
40
|
+
fixture.componentInstance.variant.set('error');
|
|
41
|
+
fixture.detectChanges();
|
|
42
|
+
expect(input.className).toContain('border-destructive');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should set aria-invalid on error', () => {
|
|
46
|
+
fixture.componentInstance.variant.set('error');
|
|
47
|
+
fixture.detectChanges();
|
|
48
|
+
expect(input.getAttribute('aria-invalid')).toBe('true');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should not set aria-invalid on default', () => {
|
|
52
|
+
expect(input.getAttribute('aria-invalid')).toBeNull();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should apply success variant', () => {
|
|
56
|
+
fixture.componentInstance.variant.set('success');
|
|
57
|
+
fixture.detectChanges();
|
|
58
|
+
expect(input.className).toContain('border-green-500');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should apply sm size', () => {
|
|
62
|
+
fixture.componentInstance.inputSize.set('sm');
|
|
63
|
+
fixture.detectChanges();
|
|
64
|
+
expect(input.className).toContain('h-9');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should apply lg size', () => {
|
|
68
|
+
fixture.componentInstance.inputSize.set('lg');
|
|
69
|
+
fixture.detectChanges();
|
|
70
|
+
expect(input.className).toContain('h-11');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should set aria-describedby', () => {
|
|
74
|
+
fixture.componentInstance.describedBy.set('help-text');
|
|
75
|
+
fixture.detectChanges();
|
|
76
|
+
expect(input.getAttribute('aria-describedby')).toBe('help-text');
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('SnyLabelDirective', () => {
|
|
81
|
+
let fixture: ComponentFixture<TestHostComponent>;
|
|
82
|
+
let label: HTMLLabelElement;
|
|
83
|
+
|
|
84
|
+
beforeEach(async () => {
|
|
85
|
+
await TestBed.configureTestingModule({
|
|
86
|
+
imports: [TestHostComponent],
|
|
87
|
+
}).compileComponents();
|
|
88
|
+
fixture = TestBed.createComponent(TestHostComponent);
|
|
89
|
+
fixture.detectChanges();
|
|
90
|
+
label = fixture.nativeElement.querySelector('label');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should apply label classes', () => {
|
|
94
|
+
expect(label.className).toContain('text-sm');
|
|
95
|
+
expect(label.className).toContain('font-medium');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should apply error variant to label', () => {
|
|
99
|
+
fixture.componentInstance.variant.set('error');
|
|
100
|
+
fixture.detectChanges();
|
|
101
|
+
expect(label.className).toContain('text-destructive');
|
|
102
|
+
});
|
|
103
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Directive, computed, input } from '@angular/core';
|
|
2
|
+
import { cn } from '../core/utils/cn';
|
|
3
|
+
import { inputVariants, type InputVariant, type InputSize } from './input.variants';
|
|
4
|
+
|
|
5
|
+
@Directive({
|
|
6
|
+
selector: 'input[snyInput], textarea[snyInput]',
|
|
7
|
+
standalone: true,
|
|
8
|
+
host: {
|
|
9
|
+
'[class]': 'computedClass()',
|
|
10
|
+
'[attr.aria-invalid]': 'variant() === "error" || null',
|
|
11
|
+
'[attr.aria-describedby]': 'ariaDescribedBy() || null',
|
|
12
|
+
},
|
|
13
|
+
})
|
|
14
|
+
export class SnyInputDirective {
|
|
15
|
+
readonly variant = input<InputVariant>('default');
|
|
16
|
+
readonly inputSize = input<InputSize>('md');
|
|
17
|
+
readonly class = input<string>('');
|
|
18
|
+
readonly ariaDescribedBy = input<string>('');
|
|
19
|
+
|
|
20
|
+
protected readonly computedClass = computed(() =>
|
|
21
|
+
cn(
|
|
22
|
+
inputVariants({ variant: this.variant(), inputSize: this.inputSize() }),
|
|
23
|
+
this.class()
|
|
24
|
+
)
|
|
25
|
+
);
|
|
26
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { cva } from 'class-variance-authority';
|
|
2
|
+
|
|
3
|
+
export const inputVariants = cva(
|
|
4
|
+
'flex w-full rounded-sm border bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium 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: 'border-destructive focus-visible:ring-destructive',
|
|
10
|
+
success: 'border-green-500 focus-visible:ring-green-500',
|
|
11
|
+
},
|
|
12
|
+
inputSize: {
|
|
13
|
+
sm: 'h-9 text-xs',
|
|
14
|
+
md: 'h-10',
|
|
15
|
+
lg: 'h-11 text-base',
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
defaultVariants: {
|
|
19
|
+
variant: 'default',
|
|
20
|
+
inputSize: 'md',
|
|
21
|
+
},
|
|
22
|
+
}
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
export const labelVariants = cva(
|
|
26
|
+
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
|
|
27
|
+
{
|
|
28
|
+
variants: {
|
|
29
|
+
variant: {
|
|
30
|
+
default: '',
|
|
31
|
+
error: 'text-destructive',
|
|
32
|
+
success: 'text-green-600',
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
defaultVariants: {
|
|
36
|
+
variant: 'default',
|
|
37
|
+
},
|
|
38
|
+
}
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
export type InputVariant = 'default' | 'error' | 'success';
|
|
42
|
+
export type InputSize = 'sm' | 'md' | 'lg';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Directive, computed, input } from '@angular/core';
|
|
2
|
+
import { cn } from '../core/utils/cn';
|
|
3
|
+
import { labelVariants, type InputVariant } from './input.variants';
|
|
4
|
+
|
|
5
|
+
@Directive({
|
|
6
|
+
selector: 'label[snyLabel]',
|
|
7
|
+
standalone: true,
|
|
8
|
+
host: { '[class]': 'computedClass()' },
|
|
9
|
+
})
|
|
10
|
+
export class SnyLabelDirective {
|
|
11
|
+
readonly variant = input<InputVariant>('default');
|
|
12
|
+
readonly class = input<string>('');
|
|
13
|
+
|
|
14
|
+
protected readonly computedClass = computed(() =>
|
|
15
|
+
cn(labelVariants({ variant: this.variant() }), this.class())
|
|
16
|
+
);
|
|
17
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Component, signal } from '@angular/core';
|
|
2
|
+
import { TestBed, type ComponentFixture } from '@angular/core/testing';
|
|
3
|
+
import { SnyKbdDirective } from './kbd.directive';
|
|
4
|
+
import type { KbdSize } from './kbd.variants';
|
|
5
|
+
|
|
6
|
+
@Component({
|
|
7
|
+
standalone: true,
|
|
8
|
+
imports: [SnyKbdDirective],
|
|
9
|
+
template: `<kbd snyKbd [size]="size()">Ctrl+K</kbd>`,
|
|
10
|
+
})
|
|
11
|
+
class TestHostComponent {
|
|
12
|
+
size = signal<KbdSize>('md');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe('SnyKbdDirective', () => {
|
|
16
|
+
let fixture: ComponentFixture<TestHostComponent>;
|
|
17
|
+
let el: HTMLElement;
|
|
18
|
+
|
|
19
|
+
beforeEach(async () => {
|
|
20
|
+
await TestBed.configureTestingModule({ imports: [TestHostComponent] }).compileComponents();
|
|
21
|
+
fixture = TestBed.createComponent(TestHostComponent);
|
|
22
|
+
fixture.detectChanges();
|
|
23
|
+
el = fixture.nativeElement.querySelector('kbd');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should apply default size classes', () => {
|
|
27
|
+
expect(el.className).toContain('font-mono');
|
|
28
|
+
expect(el.className).toContain('h-6');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should apply sm size', () => {
|
|
32
|
+
fixture.componentInstance.size.set('sm');
|
|
33
|
+
fixture.detectChanges();
|
|
34
|
+
expect(el.className).toContain('h-5');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should apply lg size', () => {
|
|
38
|
+
fixture.componentInstance.size.set('lg');
|
|
39
|
+
fixture.detectChanges();
|
|
40
|
+
expect(el.className).toContain('h-7');
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Directive, computed, input } from '@angular/core';
|
|
2
|
+
import { cn } from '../core/utils/cn';
|
|
3
|
+
import { kbdVariants, type KbdSize } from './kbd.variants';
|
|
4
|
+
|
|
5
|
+
@Directive({
|
|
6
|
+
selector: 'kbd[snyKbd]',
|
|
7
|
+
standalone: true,
|
|
8
|
+
host: {
|
|
9
|
+
'[class]': 'computedClass()',
|
|
10
|
+
},
|
|
11
|
+
})
|
|
12
|
+
export class SnyKbdDirective {
|
|
13
|
+
readonly size = input<KbdSize>('md');
|
|
14
|
+
readonly class = input<string>('');
|
|
15
|
+
|
|
16
|
+
protected readonly computedClass = computed(() =>
|
|
17
|
+
cn(kbdVariants({ size: this.size() }), this.class())
|
|
18
|
+
);
|
|
19
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { cva } from 'class-variance-authority';
|
|
2
|
+
|
|
3
|
+
export const kbdVariants = cva(
|
|
4
|
+
'pointer-events-none inline-flex select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono font-medium text-muted-foreground',
|
|
5
|
+
{
|
|
6
|
+
variants: {
|
|
7
|
+
size: {
|
|
8
|
+
sm: 'h-5 text-[10px]',
|
|
9
|
+
md: 'h-6 text-xs',
|
|
10
|
+
lg: 'h-7 text-sm',
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
defaultVariants: {
|
|
14
|
+
size: 'md',
|
|
15
|
+
},
|
|
16
|
+
}
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
export type KbdSize = 'sm' | 'md' | 'lg';
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Component, signal } from '@angular/core';
|
|
2
|
+
import { TestBed, type ComponentFixture } from '@angular/core/testing';
|
|
3
|
+
import { SnyLinkDirective } from './link.directive';
|
|
4
|
+
import type { LinkVariant } from './link.variants';
|
|
5
|
+
|
|
6
|
+
@Component({
|
|
7
|
+
standalone: true,
|
|
8
|
+
imports: [SnyLinkDirective],
|
|
9
|
+
template: `<a snyLink [variant]="variant()" href="#">Click me</a>`,
|
|
10
|
+
})
|
|
11
|
+
class TestHostComponent {
|
|
12
|
+
variant = signal<LinkVariant>('default');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe('SnyLinkDirective', () => {
|
|
16
|
+
let fixture: ComponentFixture<TestHostComponent>;
|
|
17
|
+
let el: HTMLElement;
|
|
18
|
+
|
|
19
|
+
beforeEach(async () => {
|
|
20
|
+
await TestBed.configureTestingModule({ imports: [TestHostComponent] }).compileComponents();
|
|
21
|
+
fixture = TestBed.createComponent(TestHostComponent);
|
|
22
|
+
fixture.detectChanges();
|
|
23
|
+
el = fixture.nativeElement.querySelector('a');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should apply default classes', () => {
|
|
27
|
+
expect(el.className).toContain('underline');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should apply primary variant', () => {
|
|
31
|
+
fixture.componentInstance.variant.set('primary');
|
|
32
|
+
fixture.detectChanges();
|
|
33
|
+
expect(el.className).toContain('text-primary');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should apply hover variant', () => {
|
|
37
|
+
fixture.componentInstance.variant.set('hover');
|
|
38
|
+
fixture.detectChanges();
|
|
39
|
+
expect(el.className).toContain('no-underline');
|
|
40
|
+
});
|
|
41
|
+
});
|