@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,61 @@
|
|
|
1
|
+
import { Directive, computed, input } from '@angular/core';
|
|
2
|
+
import { cn } from '../core/utils/cn';
|
|
3
|
+
|
|
4
|
+
export type NavbarVariant = 'default' | 'bordered' | 'floating';
|
|
5
|
+
|
|
6
|
+
@Directive({
|
|
7
|
+
selector: '[snyNavbar]',
|
|
8
|
+
standalone: true,
|
|
9
|
+
host: {
|
|
10
|
+
'role': 'navigation',
|
|
11
|
+
'[attr.aria-label]': 'ariaLabel()',
|
|
12
|
+
'[class]': 'computedClass()',
|
|
13
|
+
},
|
|
14
|
+
})
|
|
15
|
+
export class SnyNavbarDirective {
|
|
16
|
+
readonly variant = input<NavbarVariant>('default');
|
|
17
|
+
readonly sticky = input(false);
|
|
18
|
+
readonly ariaLabel = input<string>('Main navigation');
|
|
19
|
+
readonly class = input<string>('');
|
|
20
|
+
|
|
21
|
+
protected readonly computedClass = computed(() => {
|
|
22
|
+
const v = this.variant();
|
|
23
|
+
const base = 'flex items-center justify-between px-4 py-2 w-full bg-background';
|
|
24
|
+
const variantClass =
|
|
25
|
+
v === 'bordered' ? 'border-b border-border' :
|
|
26
|
+
v === 'floating' ? 'mx-4 mt-2 rounded-lg border border-border shadow-sm' :
|
|
27
|
+
'';
|
|
28
|
+
const stickyClass = this.sticky() ? 'sticky top-0 z-50' : '';
|
|
29
|
+
return cn(base, variantClass, stickyClass, this.class());
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@Directive({
|
|
34
|
+
selector: '[snyNavbarBrand]',
|
|
35
|
+
standalone: true,
|
|
36
|
+
host: { '[class]': 'computedClass()' },
|
|
37
|
+
})
|
|
38
|
+
export class SnyNavbarBrandDirective {
|
|
39
|
+
readonly class = input<string>('');
|
|
40
|
+
protected readonly computedClass = computed(() => cn('flex items-center gap-2 font-bold text-lg', this.class()));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@Directive({
|
|
44
|
+
selector: '[snyNavbarContent]',
|
|
45
|
+
standalone: true,
|
|
46
|
+
host: { '[class]': 'computedClass()' },
|
|
47
|
+
})
|
|
48
|
+
export class SnyNavbarContentDirective {
|
|
49
|
+
readonly class = input<string>('');
|
|
50
|
+
protected readonly computedClass = computed(() => cn('flex items-center gap-1', this.class()));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
@Directive({
|
|
54
|
+
selector: '[snyNavbarEnd]',
|
|
55
|
+
standalone: true,
|
|
56
|
+
host: { '[class]': 'computedClass()' },
|
|
57
|
+
})
|
|
58
|
+
export class SnyNavbarEndDirective {
|
|
59
|
+
readonly class = input<string>('');
|
|
60
|
+
protected readonly computedClass = computed(() => cn('flex items-center gap-2 ml-auto', this.class()));
|
|
61
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { Component, signal } from '@angular/core';
|
|
2
|
+
import { TestBed, type ComponentFixture } from '@angular/core/testing';
|
|
3
|
+
import { SnyPaginationComponent } from './pagination.component';
|
|
4
|
+
|
|
5
|
+
@Component({
|
|
6
|
+
standalone: true,
|
|
7
|
+
imports: [SnyPaginationComponent],
|
|
8
|
+
template: `<sny-pagination [(currentPage)]="currentPage" [totalPages]="totalPages()" />`,
|
|
9
|
+
})
|
|
10
|
+
class TestHostComponent {
|
|
11
|
+
currentPage = signal(1);
|
|
12
|
+
totalPages = signal(10);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe('SnyPaginationComponent', () => {
|
|
16
|
+
let fixture: ComponentFixture<TestHostComponent>;
|
|
17
|
+
let host: HTMLElement;
|
|
18
|
+
|
|
19
|
+
beforeEach(async () => {
|
|
20
|
+
await TestBed.configureTestingModule({ imports: [TestHostComponent] }).compileComponents();
|
|
21
|
+
fixture = TestBed.createComponent(TestHostComponent);
|
|
22
|
+
fixture.detectChanges();
|
|
23
|
+
host = fixture.nativeElement.querySelector('sny-pagination');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should render with navigation role', () => {
|
|
27
|
+
expect(host.getAttribute('role')).toBe('navigation');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should render page buttons', () => {
|
|
31
|
+
const buttons = host.querySelectorAll('button');
|
|
32
|
+
expect(buttons.length).toBeGreaterThan(2); // at least prev + page + next
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should mark current page with aria-current', () => {
|
|
36
|
+
const currentBtn = host.querySelector('[aria-current="page"]');
|
|
37
|
+
expect(currentBtn).toBeTruthy();
|
|
38
|
+
expect(currentBtn!.textContent?.trim()).toBe('1');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should navigate to next page', () => {
|
|
42
|
+
const nextBtn = host.querySelector('[aria-label="Go to next page"]') as HTMLButtonElement;
|
|
43
|
+
nextBtn.click();
|
|
44
|
+
fixture.detectChanges();
|
|
45
|
+
expect(fixture.componentInstance.currentPage()).toBe(2);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should disable prev on first page', () => {
|
|
49
|
+
const prevBtn = host.querySelector('[aria-label="Go to previous page"]') as HTMLButtonElement;
|
|
50
|
+
expect(prevBtn.disabled).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should disable next on last page', () => {
|
|
54
|
+
fixture.componentInstance.currentPage.set(10);
|
|
55
|
+
fixture.detectChanges();
|
|
56
|
+
const nextBtn = host.querySelector('[aria-label="Go to next page"]') as HTMLButtonElement;
|
|
57
|
+
expect(nextBtn.disabled).toBe(true);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { ChangeDetectionStrategy, Component, computed, input, model } from '@angular/core';
|
|
2
|
+
import { cn } from '../core/utils/cn';
|
|
3
|
+
import {
|
|
4
|
+
paginationItemVariants,
|
|
5
|
+
type PaginationVariant,
|
|
6
|
+
type PaginationSize,
|
|
7
|
+
} from './pagination.variants';
|
|
8
|
+
|
|
9
|
+
function computePageRange(
|
|
10
|
+
totalPages: number,
|
|
11
|
+
currentPage: number,
|
|
12
|
+
siblingCount: number,
|
|
13
|
+
boundaryCount: number
|
|
14
|
+
): (number | 'ellipsis')[] {
|
|
15
|
+
const range = (start: number, end: number) =>
|
|
16
|
+
Array.from({ length: end - start + 1 }, (_, i) => start + i);
|
|
17
|
+
|
|
18
|
+
const startPages = range(1, Math.min(boundaryCount, totalPages));
|
|
19
|
+
const endPages = range(Math.max(totalPages - boundaryCount + 1, boundaryCount + 1), totalPages);
|
|
20
|
+
|
|
21
|
+
const siblingsStart = Math.max(
|
|
22
|
+
Math.min(currentPage - siblingCount, totalPages - boundaryCount - siblingCount * 2 - 1),
|
|
23
|
+
boundaryCount + 2
|
|
24
|
+
);
|
|
25
|
+
const siblingsEnd = Math.min(
|
|
26
|
+
Math.max(currentPage + siblingCount, boundaryCount + siblingCount * 2 + 2),
|
|
27
|
+
endPages.length > 0 ? endPages[0] - 2 : totalPages - 1
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
const result: (number | 'ellipsis')[] = [...startPages];
|
|
31
|
+
|
|
32
|
+
if (siblingsStart > boundaryCount + 2) {
|
|
33
|
+
result.push('ellipsis');
|
|
34
|
+
} else if (boundaryCount + 1 < totalPages - boundaryCount) {
|
|
35
|
+
result.push(boundaryCount + 1);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
result.push(...range(siblingsStart, siblingsEnd));
|
|
39
|
+
|
|
40
|
+
if (siblingsEnd < totalPages - boundaryCount - 1) {
|
|
41
|
+
result.push('ellipsis');
|
|
42
|
+
} else if (totalPages - boundaryCount > boundaryCount) {
|
|
43
|
+
result.push(totalPages - boundaryCount);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
result.push(...endPages);
|
|
47
|
+
|
|
48
|
+
return [...new Set(result)].sort((a, b) => {
|
|
49
|
+
if (a === 'ellipsis') return 0;
|
|
50
|
+
if (b === 'ellipsis') return 0;
|
|
51
|
+
return a - b;
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@Component({
|
|
56
|
+
selector: 'sny-pagination',
|
|
57
|
+
standalone: true,
|
|
58
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
59
|
+
host: {
|
|
60
|
+
'role': 'navigation',
|
|
61
|
+
'aria-label': 'Pagination',
|
|
62
|
+
},
|
|
63
|
+
template: `
|
|
64
|
+
<div class="flex items-center gap-1">
|
|
65
|
+
<button
|
|
66
|
+
[class]="navBtnClass()"
|
|
67
|
+
[disabled]="!hasPrev()"
|
|
68
|
+
[attr.aria-label]="'Go to previous page'"
|
|
69
|
+
(click)="prev()"
|
|
70
|
+
>
|
|
71
|
+
<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="m15 18-6-6 6-6"/></svg>
|
|
72
|
+
</button>
|
|
73
|
+
|
|
74
|
+
@for (page of pages(); track $index) {
|
|
75
|
+
@if (page === 'ellipsis') {
|
|
76
|
+
<span class="flex h-9 w-9 items-center justify-center" aria-hidden="true">...</span>
|
|
77
|
+
} @else {
|
|
78
|
+
<button
|
|
79
|
+
[class]="pageClass(page)"
|
|
80
|
+
[attr.aria-label]="'Page ' + page"
|
|
81
|
+
[attr.aria-current]="page === currentPage() ? 'page' : null"
|
|
82
|
+
(click)="goToPage(page)"
|
|
83
|
+
>
|
|
84
|
+
{{ page }}
|
|
85
|
+
</button>
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
<button
|
|
90
|
+
[class]="navBtnClass()"
|
|
91
|
+
[disabled]="!hasNext()"
|
|
92
|
+
[attr.aria-label]="'Go to next page'"
|
|
93
|
+
(click)="next()"
|
|
94
|
+
>
|
|
95
|
+
<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="m9 18 6-6-6-6"/></svg>
|
|
96
|
+
</button>
|
|
97
|
+
</div>
|
|
98
|
+
`,
|
|
99
|
+
})
|
|
100
|
+
export class SnyPaginationComponent {
|
|
101
|
+
readonly currentPage = model(1);
|
|
102
|
+
readonly totalPages = input.required<number>();
|
|
103
|
+
readonly siblingCount = input(1);
|
|
104
|
+
readonly boundaryCount = input(1);
|
|
105
|
+
readonly size = input<PaginationSize>('md');
|
|
106
|
+
readonly variant = input<PaginationVariant>('default');
|
|
107
|
+
readonly class = input<string>('');
|
|
108
|
+
|
|
109
|
+
readonly pages = computed(() =>
|
|
110
|
+
computePageRange(this.totalPages(), this.currentPage(), this.siblingCount(), this.boundaryCount())
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
readonly hasPrev = computed(() => this.currentPage() > 1);
|
|
114
|
+
readonly hasNext = computed(() => this.currentPage() < this.totalPages());
|
|
115
|
+
|
|
116
|
+
goToPage(page: number | 'ellipsis'): void {
|
|
117
|
+
if (page === 'ellipsis') return;
|
|
118
|
+
this.currentPage.set(page);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
prev(): void {
|
|
122
|
+
if (this.hasPrev()) this.currentPage.update((p) => p - 1);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
next(): void {
|
|
126
|
+
if (this.hasNext()) this.currentPage.update((p) => p + 1);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
pageClass(page: number): string {
|
|
130
|
+
return cn(
|
|
131
|
+
paginationItemVariants({
|
|
132
|
+
variant: this.variant(),
|
|
133
|
+
size: this.size(),
|
|
134
|
+
active: page === this.currentPage(),
|
|
135
|
+
})
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
navBtnClass(): string {
|
|
140
|
+
return cn(
|
|
141
|
+
paginationItemVariants({ variant: this.variant(), size: this.size(), active: false })
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { cva } from 'class-variance-authority';
|
|
2
|
+
|
|
3
|
+
export const paginationItemVariants = cva(
|
|
4
|
+
'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50',
|
|
5
|
+
{
|
|
6
|
+
variants: {
|
|
7
|
+
variant: {
|
|
8
|
+
default: 'bg-background hover:bg-accent hover:text-accent-foreground',
|
|
9
|
+
outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
|
|
10
|
+
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
|
11
|
+
},
|
|
12
|
+
size: {
|
|
13
|
+
sm: 'h-8 w-8 text-xs',
|
|
14
|
+
md: 'h-9 w-9',
|
|
15
|
+
lg: 'h-10 w-10',
|
|
16
|
+
},
|
|
17
|
+
active: {
|
|
18
|
+
true: 'bg-primary text-primary-foreground hover:bg-primary/90 hover:text-primary-foreground',
|
|
19
|
+
false: '',
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
defaultVariants: {
|
|
23
|
+
variant: 'default',
|
|
24
|
+
size: 'md',
|
|
25
|
+
active: false,
|
|
26
|
+
},
|
|
27
|
+
}
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
export type PaginationVariant = 'default' | 'outline' | 'ghost';
|
|
31
|
+
export type PaginationSize = 'sm' | 'md' | 'lg';
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { Component, signal } from '@angular/core';
|
|
2
|
+
import { TestBed, type ComponentFixture } from '@angular/core/testing';
|
|
3
|
+
import { SnyProgressComponent } from './progress.component';
|
|
4
|
+
import type { ProgressVariant, ProgressSize } from './progress.variants';
|
|
5
|
+
|
|
6
|
+
@Component({
|
|
7
|
+
standalone: true,
|
|
8
|
+
imports: [SnyProgressComponent],
|
|
9
|
+
template: `
|
|
10
|
+
<sny-progress
|
|
11
|
+
[value]="value()"
|
|
12
|
+
[max]="max()"
|
|
13
|
+
[variant]="variant()"
|
|
14
|
+
[size]="size()"
|
|
15
|
+
[indeterminate]="indeterminate()"
|
|
16
|
+
[label]="label()"
|
|
17
|
+
/>
|
|
18
|
+
`,
|
|
19
|
+
})
|
|
20
|
+
class TestHostComponent {
|
|
21
|
+
value = signal(50);
|
|
22
|
+
max = signal(100);
|
|
23
|
+
variant = signal<ProgressVariant>('default');
|
|
24
|
+
size = signal<ProgressSize>('md');
|
|
25
|
+
indeterminate = signal(false);
|
|
26
|
+
label = signal('Loading');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('SnyProgressComponent', () => {
|
|
30
|
+
let fixture: ComponentFixture<TestHostComponent>;
|
|
31
|
+
let host: HTMLElement;
|
|
32
|
+
|
|
33
|
+
beforeEach(async () => {
|
|
34
|
+
await TestBed.configureTestingModule({
|
|
35
|
+
imports: [TestHostComponent],
|
|
36
|
+
}).compileComponents();
|
|
37
|
+
fixture = TestBed.createComponent(TestHostComponent);
|
|
38
|
+
fixture.detectChanges();
|
|
39
|
+
host = fixture.nativeElement.querySelector('sny-progress');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should render with progressbar role', () => {
|
|
43
|
+
expect(host.getAttribute('role')).toBe('progressbar');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should set aria-valuenow to current value', () => {
|
|
47
|
+
expect(host.getAttribute('aria-valuenow')).toBe('50');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should set aria-valuemin to 0', () => {
|
|
51
|
+
expect(host.getAttribute('aria-valuemin')).toBe('0');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should set aria-valuemax to max value', () => {
|
|
55
|
+
expect(host.getAttribute('aria-valuemax')).toBe('100');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should set aria-label', () => {
|
|
59
|
+
expect(host.getAttribute('aria-label')).toBe('Loading');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should calculate percentage correctly', () => {
|
|
63
|
+
const bar = host.querySelector('div > div') as HTMLElement;
|
|
64
|
+
expect(bar.style.width).toBe('50%');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should update percentage when value changes', () => {
|
|
68
|
+
fixture.componentInstance.value.set(75);
|
|
69
|
+
fixture.detectChanges();
|
|
70
|
+
const bar = host.querySelector('div > div') as HTMLElement;
|
|
71
|
+
expect(bar.style.width).toBe('75%');
|
|
72
|
+
expect(host.getAttribute('aria-valuenow')).toBe('75');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should cap percentage at 100%', () => {
|
|
76
|
+
fixture.componentInstance.value.set(150);
|
|
77
|
+
fixture.detectChanges();
|
|
78
|
+
const bar = host.querySelector('div > div') as HTMLElement;
|
|
79
|
+
expect(bar.style.width).toBe('100%');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should omit aria-valuenow when indeterminate', () => {
|
|
83
|
+
fixture.componentInstance.indeterminate.set(true);
|
|
84
|
+
fixture.detectChanges();
|
|
85
|
+
expect(host.getAttribute('aria-valuenow')).toBeNull();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should apply indeterminate animation class', () => {
|
|
89
|
+
fixture.componentInstance.indeterminate.set(true);
|
|
90
|
+
fixture.detectChanges();
|
|
91
|
+
const bar = host.querySelector('div > div') as HTMLElement;
|
|
92
|
+
expect(bar.className).toContain('animate-progress-indeterminate');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should apply size variant', () => {
|
|
96
|
+
fixture.componentInstance.size.set('lg');
|
|
97
|
+
fixture.detectChanges();
|
|
98
|
+
const track = host.querySelector('div') as HTMLElement;
|
|
99
|
+
expect(track.className).toContain('h-4');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should apply success variant', () => {
|
|
103
|
+
fixture.componentInstance.variant.set('success');
|
|
104
|
+
fixture.detectChanges();
|
|
105
|
+
const bar = host.querySelector('div > div') as HTMLElement;
|
|
106
|
+
expect(bar.className).toContain('bg-green-600');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should work with custom max value', () => {
|
|
110
|
+
fixture.componentInstance.max.set(200);
|
|
111
|
+
fixture.componentInstance.value.set(100);
|
|
112
|
+
fixture.detectChanges();
|
|
113
|
+
const bar = host.querySelector('div > div') as HTMLElement;
|
|
114
|
+
expect(bar.style.width).toBe('50%');
|
|
115
|
+
expect(host.getAttribute('aria-valuemax')).toBe('200');
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
|
|
2
|
+
import { cn } from '../core/utils/cn';
|
|
3
|
+
import {
|
|
4
|
+
progressTrackVariants,
|
|
5
|
+
progressBarVariants,
|
|
6
|
+
type ProgressVariant,
|
|
7
|
+
type ProgressSize,
|
|
8
|
+
} from './progress.variants';
|
|
9
|
+
|
|
10
|
+
@Component({
|
|
11
|
+
selector: 'sny-progress',
|
|
12
|
+
standalone: true,
|
|
13
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
14
|
+
host: {
|
|
15
|
+
'role': 'progressbar',
|
|
16
|
+
'[attr.aria-valuenow]': 'indeterminate() ? null : value()',
|
|
17
|
+
'[attr.aria-valuemin]': '0',
|
|
18
|
+
'[attr.aria-valuemax]': 'max()',
|
|
19
|
+
'[attr.aria-label]': 'label()',
|
|
20
|
+
'[class]': '"w-full"',
|
|
21
|
+
},
|
|
22
|
+
template: `
|
|
23
|
+
<div [class]="trackClass()">
|
|
24
|
+
<div
|
|
25
|
+
[class]="barClass()"
|
|
26
|
+
[style.width.%]="indeterminate() ? null : percentage()"
|
|
27
|
+
></div>
|
|
28
|
+
</div>
|
|
29
|
+
`,
|
|
30
|
+
styles: `
|
|
31
|
+
@keyframes progress-indeterminate {
|
|
32
|
+
0% { transform: translateX(-100%); }
|
|
33
|
+
100% { transform: translateX(400%); }
|
|
34
|
+
}
|
|
35
|
+
:host .animate-progress-indeterminate {
|
|
36
|
+
animation: progress-indeterminate 1.5s ease-in-out infinite;
|
|
37
|
+
}
|
|
38
|
+
`,
|
|
39
|
+
})
|
|
40
|
+
export class SnyProgressComponent {
|
|
41
|
+
readonly value = input(0);
|
|
42
|
+
readonly max = input(100);
|
|
43
|
+
readonly variant = input<ProgressVariant>('default');
|
|
44
|
+
readonly size = input<ProgressSize>('md');
|
|
45
|
+
readonly indeterminate = input(false);
|
|
46
|
+
readonly label = input('Progress');
|
|
47
|
+
readonly class = input<string>('');
|
|
48
|
+
|
|
49
|
+
readonly percentage = computed(() =>
|
|
50
|
+
Math.min(100, (this.value() / this.max()) * 100)
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
protected readonly trackClass = computed(() =>
|
|
54
|
+
cn(progressTrackVariants({ size: this.size() }), this.class())
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
protected readonly barClass = computed(() =>
|
|
58
|
+
cn(
|
|
59
|
+
progressBarVariants({
|
|
60
|
+
variant: this.variant(),
|
|
61
|
+
indeterminate: this.indeterminate(),
|
|
62
|
+
})
|
|
63
|
+
)
|
|
64
|
+
);
|
|
65
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { cva } from 'class-variance-authority';
|
|
2
|
+
|
|
3
|
+
export const progressTrackVariants = cva(
|
|
4
|
+
'relative w-full overflow-hidden rounded-full bg-secondary',
|
|
5
|
+
{
|
|
6
|
+
variants: {
|
|
7
|
+
size: {
|
|
8
|
+
sm: 'h-1.5',
|
|
9
|
+
md: 'h-2.5',
|
|
10
|
+
lg: 'h-4',
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
defaultVariants: {
|
|
14
|
+
size: 'md',
|
|
15
|
+
},
|
|
16
|
+
}
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
export const progressBarVariants = cva(
|
|
20
|
+
'h-full rounded-full transition-all duration-300 ease-in-out',
|
|
21
|
+
{
|
|
22
|
+
variants: {
|
|
23
|
+
variant: {
|
|
24
|
+
default: 'bg-primary',
|
|
25
|
+
success: 'bg-green-600 dark:bg-green-500',
|
|
26
|
+
warning: 'bg-yellow-500 dark:bg-yellow-400',
|
|
27
|
+
error: 'bg-destructive',
|
|
28
|
+
info: 'bg-blue-600 dark:bg-blue-500',
|
|
29
|
+
},
|
|
30
|
+
indeterminate: {
|
|
31
|
+
true: 'animate-progress-indeterminate w-1/3',
|
|
32
|
+
false: '',
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
defaultVariants: {
|
|
36
|
+
variant: 'default',
|
|
37
|
+
indeterminate: false,
|
|
38
|
+
},
|
|
39
|
+
}
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
export type ProgressVariant = 'default' | 'success' | 'warning' | 'error' | 'info';
|
|
43
|
+
export type ProgressSize = '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 { SnyRadialProgressComponent } from './radial-progress.component';
|
|
4
|
+
|
|
5
|
+
@Component({
|
|
6
|
+
standalone: true,
|
|
7
|
+
imports: [SnyRadialProgressComponent],
|
|
8
|
+
template: `<sny-radial-progress [value]="value()">{{ value() }}%</sny-radial-progress>`,
|
|
9
|
+
})
|
|
10
|
+
class TestHostComponent {
|
|
11
|
+
value = signal(75);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe('SnyRadialProgressComponent', () => {
|
|
15
|
+
let fixture: ComponentFixture<TestHostComponent>;
|
|
16
|
+
let host: HTMLElement;
|
|
17
|
+
|
|
18
|
+
beforeEach(async () => {
|
|
19
|
+
await TestBed.configureTestingModule({ imports: [TestHostComponent] }).compileComponents();
|
|
20
|
+
fixture = TestBed.createComponent(TestHostComponent);
|
|
21
|
+
fixture.detectChanges();
|
|
22
|
+
host = fixture.nativeElement.querySelector('sny-radial-progress');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should render with progressbar role', () => {
|
|
26
|
+
expect(host.getAttribute('role')).toBe('progressbar');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should set aria-valuenow', () => {
|
|
30
|
+
expect(host.getAttribute('aria-valuenow')).toBe('75');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should render SVG circles', () => {
|
|
34
|
+
const circles = host.querySelectorAll('circle');
|
|
35
|
+
expect(circles.length).toBe(2);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should project content', () => {
|
|
39
|
+
expect(host.textContent).toContain('75%');
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
|
|
2
|
+
import { cn } from '../core/utils/cn';
|
|
3
|
+
|
|
4
|
+
export type RadialProgressSize = 'sm' | 'md' | 'lg';
|
|
5
|
+
export type RadialProgressVariant = 'default' | 'success' | 'warning' | 'error' | 'info';
|
|
6
|
+
|
|
7
|
+
const sizeMap: Record<RadialProgressSize, number> = { sm: 48, md: 72, lg: 96 };
|
|
8
|
+
|
|
9
|
+
const variantColorMap: Record<RadialProgressVariant, string> = {
|
|
10
|
+
default: 'stroke-primary',
|
|
11
|
+
success: 'stroke-green-600 dark:stroke-green-500',
|
|
12
|
+
warning: 'stroke-yellow-500',
|
|
13
|
+
error: 'stroke-destructive',
|
|
14
|
+
info: 'stroke-blue-600 dark:stroke-blue-500',
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
@Component({
|
|
18
|
+
selector: 'sny-radial-progress',
|
|
19
|
+
standalone: true,
|
|
20
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
21
|
+
host: {
|
|
22
|
+
'role': 'progressbar',
|
|
23
|
+
'[attr.aria-valuenow]': 'value()',
|
|
24
|
+
'[attr.aria-valuemin]': '0',
|
|
25
|
+
'[attr.aria-valuemax]': '100',
|
|
26
|
+
'[class]': '"inline-flex items-center justify-center"',
|
|
27
|
+
},
|
|
28
|
+
template: `
|
|
29
|
+
<div class="relative" [style.width.px]="svgSize()" [style.height.px]="svgSize()">
|
|
30
|
+
<svg [attr.width]="svgSize()" [attr.height]="svgSize()" class="-rotate-90">
|
|
31
|
+
<circle
|
|
32
|
+
[attr.cx]="svgSize() / 2"
|
|
33
|
+
[attr.cy]="svgSize() / 2"
|
|
34
|
+
[attr.r]="radius()"
|
|
35
|
+
fill="none"
|
|
36
|
+
class="stroke-muted"
|
|
37
|
+
[attr.stroke-width]="thickness()"
|
|
38
|
+
/>
|
|
39
|
+
<circle
|
|
40
|
+
[attr.cx]="svgSize() / 2"
|
|
41
|
+
[attr.cy]="svgSize() / 2"
|
|
42
|
+
[attr.r]="radius()"
|
|
43
|
+
fill="none"
|
|
44
|
+
[class]="strokeClass()"
|
|
45
|
+
[attr.stroke-width]="thickness()"
|
|
46
|
+
[attr.stroke-dasharray]="circumference()"
|
|
47
|
+
[attr.stroke-dashoffset]="offset()"
|
|
48
|
+
stroke-linecap="round"
|
|
49
|
+
class="transition-all duration-300"
|
|
50
|
+
/>
|
|
51
|
+
</svg>
|
|
52
|
+
<div class="absolute inset-0 flex items-center justify-center">
|
|
53
|
+
<ng-content />
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
`,
|
|
57
|
+
})
|
|
58
|
+
export class SnyRadialProgressComponent {
|
|
59
|
+
readonly value = input(0);
|
|
60
|
+
readonly size = input<RadialProgressSize>('md');
|
|
61
|
+
readonly thickness = input(4);
|
|
62
|
+
readonly variant = input<RadialProgressVariant>('default');
|
|
63
|
+
readonly class = input<string>('');
|
|
64
|
+
|
|
65
|
+
readonly svgSize = computed(() => sizeMap[this.size()]);
|
|
66
|
+
readonly radius = computed(() => (this.svgSize() - this.thickness()) / 2);
|
|
67
|
+
readonly circumference = computed(() => 2 * Math.PI * this.radius());
|
|
68
|
+
readonly offset = computed(() => this.circumference() - (this.value() / 100) * this.circumference());
|
|
69
|
+
|
|
70
|
+
readonly strokeClass = computed(() => variantColorMap[this.variant()]);
|
|
71
|
+
}
|