@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,48 @@
|
|
|
1
|
+
import { Component, signal } from '@angular/core';
|
|
2
|
+
import { TestBed, type ComponentFixture } from '@angular/core/testing';
|
|
3
|
+
import { SnyDividerComponent } from './divider.component';
|
|
4
|
+
import type { DividerOrientation } from './divider.variants';
|
|
5
|
+
|
|
6
|
+
@Component({
|
|
7
|
+
standalone: true,
|
|
8
|
+
imports: [SnyDividerComponent],
|
|
9
|
+
template: `<sny-divider [orientation]="orientation()" [label]="label()" />`,
|
|
10
|
+
})
|
|
11
|
+
class TestHostComponent {
|
|
12
|
+
orientation = signal<DividerOrientation>('horizontal');
|
|
13
|
+
label = signal('');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe('SnyDividerComponent', () => {
|
|
17
|
+
let fixture: ComponentFixture<TestHostComponent>;
|
|
18
|
+
let host: HTMLElement;
|
|
19
|
+
|
|
20
|
+
beforeEach(async () => {
|
|
21
|
+
await TestBed.configureTestingModule({ imports: [TestHostComponent] }).compileComponents();
|
|
22
|
+
fixture = TestBed.createComponent(TestHostComponent);
|
|
23
|
+
fixture.detectChanges();
|
|
24
|
+
host = fixture.nativeElement.querySelector('sny-divider');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should have separator role', () => {
|
|
28
|
+
expect(host.getAttribute('role')).toBe('separator');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should set aria-orientation', () => {
|
|
32
|
+
expect(host.getAttribute('aria-orientation')).toBe('horizontal');
|
|
33
|
+
fixture.componentInstance.orientation.set('vertical');
|
|
34
|
+
fixture.detectChanges();
|
|
35
|
+
expect(host.getAttribute('aria-orientation')).toBe('vertical');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should render simple divider without label', () => {
|
|
39
|
+
const div = host.querySelector('div');
|
|
40
|
+
expect(div!.className).toContain('bg-border');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should render label when provided', () => {
|
|
44
|
+
fixture.componentInstance.label.set('OR');
|
|
45
|
+
fixture.detectChanges();
|
|
46
|
+
expect(host.textContent).toContain('OR');
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
|
|
2
|
+
import { cn } from '../core/utils/cn';
|
|
3
|
+
import {
|
|
4
|
+
dividerVariants,
|
|
5
|
+
type DividerOrientation,
|
|
6
|
+
type DividerVariant,
|
|
7
|
+
} from './divider.variants';
|
|
8
|
+
|
|
9
|
+
@Component({
|
|
10
|
+
selector: 'sny-divider',
|
|
11
|
+
standalone: true,
|
|
12
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
13
|
+
host: {
|
|
14
|
+
'role': 'separator',
|
|
15
|
+
'[attr.aria-orientation]': 'orientation()',
|
|
16
|
+
},
|
|
17
|
+
template: `
|
|
18
|
+
@if (hasLabel()) {
|
|
19
|
+
<div [class]="labelContainerClass()">
|
|
20
|
+
<div [class]="lineClass()"></div>
|
|
21
|
+
<span class="px-2 text-xs text-muted-foreground">{{ label() }}</span>
|
|
22
|
+
<div [class]="lineClass()"></div>
|
|
23
|
+
</div>
|
|
24
|
+
} @else {
|
|
25
|
+
<div [class]="dividerClass()"></div>
|
|
26
|
+
}
|
|
27
|
+
`,
|
|
28
|
+
})
|
|
29
|
+
export class SnyDividerComponent {
|
|
30
|
+
readonly orientation = input<DividerOrientation>('horizontal');
|
|
31
|
+
readonly variant = input<DividerVariant>('solid');
|
|
32
|
+
readonly label = input<string>('');
|
|
33
|
+
readonly class = input<string>('');
|
|
34
|
+
|
|
35
|
+
readonly hasLabel = computed(() => !!this.label());
|
|
36
|
+
|
|
37
|
+
protected readonly dividerClass = computed(() =>
|
|
38
|
+
cn(dividerVariants({ orientation: this.orientation(), variant: this.variant() }), this.class())
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
protected readonly lineClass = computed(() =>
|
|
42
|
+
cn('flex-1 bg-border', this.orientation() === 'horizontal' ? 'h-[1px]' : 'w-[1px]')
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
protected readonly labelContainerClass = computed(() =>
|
|
46
|
+
cn(
|
|
47
|
+
'flex items-center',
|
|
48
|
+
this.orientation() === 'horizontal' ? 'flex-row' : 'flex-col',
|
|
49
|
+
this.class()
|
|
50
|
+
)
|
|
51
|
+
);
|
|
52
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { cva } from 'class-variance-authority';
|
|
2
|
+
|
|
3
|
+
export const dividerVariants = cva('shrink-0 bg-border', {
|
|
4
|
+
variants: {
|
|
5
|
+
orientation: {
|
|
6
|
+
horizontal: 'h-[1px] w-full',
|
|
7
|
+
vertical: 'h-full w-[1px]',
|
|
8
|
+
},
|
|
9
|
+
variant: {
|
|
10
|
+
solid: '',
|
|
11
|
+
dashed: 'border-dashed',
|
|
12
|
+
dotted: 'border-dotted',
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
defaultVariants: {
|
|
16
|
+
orientation: 'horizontal',
|
|
17
|
+
variant: 'solid',
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export type DividerOrientation = 'horizontal' | 'vertical';
|
|
22
|
+
export type DividerVariant = 'solid' | 'dashed' | 'dotted';
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { Component, signal } from '@angular/core';
|
|
2
|
+
import { TestBed, type ComponentFixture } from '@angular/core/testing';
|
|
3
|
+
import { SnyDockDirective, SnyDockItemDirective, type DockPosition } from './dock.directives';
|
|
4
|
+
|
|
5
|
+
@Component({
|
|
6
|
+
standalone: true,
|
|
7
|
+
imports: [SnyDockDirective, SnyDockItemDirective],
|
|
8
|
+
template: `
|
|
9
|
+
<div snyDock [position]="position()">
|
|
10
|
+
<button snyDockItem [active]="true">Home</button>
|
|
11
|
+
<button snyDockItem>Settings</button>
|
|
12
|
+
</div>
|
|
13
|
+
`,
|
|
14
|
+
})
|
|
15
|
+
class TestHostComponent {
|
|
16
|
+
position = signal<DockPosition>('bottom');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe('SnyDockDirective', () => {
|
|
20
|
+
let fixture: ComponentFixture<TestHostComponent>;
|
|
21
|
+
|
|
22
|
+
beforeEach(async () => {
|
|
23
|
+
await TestBed.configureTestingModule({ imports: [TestHostComponent] }).compileComponents();
|
|
24
|
+
fixture = TestBed.createComponent(TestHostComponent);
|
|
25
|
+
fixture.detectChanges();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should render with toolbar role', () => {
|
|
29
|
+
const dock = fixture.nativeElement.querySelector('[snyDock]');
|
|
30
|
+
expect(dock.getAttribute('role')).toBe('toolbar');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should position at bottom by default', () => {
|
|
34
|
+
const dock = fixture.nativeElement.querySelector('[snyDock]');
|
|
35
|
+
expect(dock.className).toContain('bottom-4');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should position at top', () => {
|
|
39
|
+
fixture.componentInstance.position.set('top');
|
|
40
|
+
fixture.detectChanges();
|
|
41
|
+
const dock = fixture.nativeElement.querySelector('[snyDock]');
|
|
42
|
+
expect(dock.className).toContain('top-4');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should apply active state to dock item', () => {
|
|
46
|
+
const items = fixture.nativeElement.querySelectorAll('[snyDockItem]');
|
|
47
|
+
expect(items[0].className).toContain('bg-primary');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should move focus with ArrowRight', () => {
|
|
51
|
+
const items = fixture.nativeElement.querySelectorAll('[snyDockItem]');
|
|
52
|
+
(items[0] as HTMLElement).focus();
|
|
53
|
+
items[0].dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true }));
|
|
54
|
+
fixture.detectChanges();
|
|
55
|
+
const updated = fixture.nativeElement.querySelectorAll('[snyDockItem]');
|
|
56
|
+
expect(document.activeElement).toBe(updated[1]);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should move focus with ArrowLeft', () => {
|
|
60
|
+
const items = fixture.nativeElement.querySelectorAll('[snyDockItem]');
|
|
61
|
+
(items[1] as HTMLElement).focus();
|
|
62
|
+
items[1].dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowLeft', bubbles: true }));
|
|
63
|
+
fixture.detectChanges();
|
|
64
|
+
const updated = fixture.nativeElement.querySelectorAll('[snyDockItem]');
|
|
65
|
+
expect(document.activeElement).toBe(updated[0]);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should move focus to first with Home', () => {
|
|
69
|
+
const items = fixture.nativeElement.querySelectorAll('[snyDockItem]');
|
|
70
|
+
(items[1] as HTMLElement).focus();
|
|
71
|
+
items[1].dispatchEvent(new KeyboardEvent('keydown', { key: 'Home', bubbles: true }));
|
|
72
|
+
fixture.detectChanges();
|
|
73
|
+
const updated = fixture.nativeElement.querySelectorAll('[snyDockItem]');
|
|
74
|
+
expect(document.activeElement).toBe(updated[0]);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should move focus to last with End', () => {
|
|
78
|
+
const items = fixture.nativeElement.querySelectorAll('[snyDockItem]');
|
|
79
|
+
(items[0] as HTMLElement).focus();
|
|
80
|
+
items[0].dispatchEvent(new KeyboardEvent('keydown', { key: 'End', bubbles: true }));
|
|
81
|
+
fixture.detectChanges();
|
|
82
|
+
const updated = fixture.nativeElement.querySelectorAll('[snyDockItem]');
|
|
83
|
+
expect(document.activeElement).toBe(updated[1]);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { Directive, ElementRef, computed, inject, input } from '@angular/core';
|
|
2
|
+
import { cn } from '../core/utils/cn';
|
|
3
|
+
|
|
4
|
+
export type DockPosition = 'bottom' | 'top';
|
|
5
|
+
|
|
6
|
+
@Directive({
|
|
7
|
+
selector: '[snyDock]',
|
|
8
|
+
standalone: true,
|
|
9
|
+
host: {
|
|
10
|
+
'role': 'toolbar',
|
|
11
|
+
'aria-label': 'Dock',
|
|
12
|
+
'[class]': 'computedClass()',
|
|
13
|
+
'(keydown)': 'onKeydown($event)',
|
|
14
|
+
},
|
|
15
|
+
})
|
|
16
|
+
export class SnyDockDirective {
|
|
17
|
+
readonly position = input<DockPosition>('bottom');
|
|
18
|
+
readonly class = input<string>('');
|
|
19
|
+
|
|
20
|
+
private readonly elRef = inject(ElementRef);
|
|
21
|
+
|
|
22
|
+
protected readonly computedClass = computed(() =>
|
|
23
|
+
cn(
|
|
24
|
+
'fixed left-1/2 -translate-x-1/2 z-50 flex items-center gap-1 rounded-full border bg-background/80 backdrop-blur-sm px-3 py-2 shadow-lg',
|
|
25
|
+
this.position() === 'bottom' ? 'bottom-4' : 'top-4',
|
|
26
|
+
this.class()
|
|
27
|
+
)
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
onKeydown(event: KeyboardEvent): void {
|
|
31
|
+
const items = Array.from(
|
|
32
|
+
(this.elRef.nativeElement as HTMLElement).querySelectorAll<HTMLElement>('[snyDockItem]')
|
|
33
|
+
);
|
|
34
|
+
if (items.length === 0) return;
|
|
35
|
+
|
|
36
|
+
const currentIndex = items.indexOf(document.activeElement as HTMLElement);
|
|
37
|
+
if (currentIndex === -1) return;
|
|
38
|
+
|
|
39
|
+
let nextIndex: number | null = null;
|
|
40
|
+
switch (event.key) {
|
|
41
|
+
case 'ArrowRight':
|
|
42
|
+
event.preventDefault();
|
|
43
|
+
nextIndex = (currentIndex + 1) % items.length;
|
|
44
|
+
break;
|
|
45
|
+
case 'ArrowLeft':
|
|
46
|
+
event.preventDefault();
|
|
47
|
+
nextIndex = (currentIndex - 1 + items.length) % items.length;
|
|
48
|
+
break;
|
|
49
|
+
case 'Home':
|
|
50
|
+
event.preventDefault();
|
|
51
|
+
nextIndex = 0;
|
|
52
|
+
break;
|
|
53
|
+
case 'End':
|
|
54
|
+
event.preventDefault();
|
|
55
|
+
nextIndex = items.length - 1;
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
if (nextIndex !== null) {
|
|
59
|
+
items[nextIndex].focus();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
@Directive({
|
|
65
|
+
selector: '[snyDockItem]',
|
|
66
|
+
standalone: true,
|
|
67
|
+
host: {
|
|
68
|
+
'[class]': 'computedClass()',
|
|
69
|
+
'[attr.tabindex]': 'active() ? 0 : -1',
|
|
70
|
+
},
|
|
71
|
+
})
|
|
72
|
+
export class SnyDockItemDirective {
|
|
73
|
+
readonly active = input(false);
|
|
74
|
+
readonly class = input<string>('');
|
|
75
|
+
|
|
76
|
+
protected readonly computedClass = computed(() =>
|
|
77
|
+
cn(
|
|
78
|
+
'inline-flex items-center justify-center rounded-full p-2 transition-all hover:scale-110',
|
|
79
|
+
this.active() && 'bg-primary text-primary-foreground',
|
|
80
|
+
this.class()
|
|
81
|
+
)
|
|
82
|
+
);
|
|
83
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { SnyDockDirective, SnyDockItemDirective, type DockPosition } from './dock.directives';
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Component, viewChild } from '@angular/core';
|
|
2
|
+
import { TestBed, type ComponentFixture } from '@angular/core/testing';
|
|
3
|
+
import { SnyDrawerLayoutDirective, SnyDrawerContentDirective, SnyDrawerSideDirective } from './drawer.directives';
|
|
4
|
+
|
|
5
|
+
@Component({
|
|
6
|
+
standalone: true,
|
|
7
|
+
imports: [SnyDrawerLayoutDirective, SnyDrawerContentDirective, SnyDrawerSideDirective],
|
|
8
|
+
template: `
|
|
9
|
+
<div snyDrawerLayout #drawer="snyDrawerLayout">
|
|
10
|
+
<div snyDrawerSide>Sidebar</div>
|
|
11
|
+
<div snyDrawerContent>Main</div>
|
|
12
|
+
</div>
|
|
13
|
+
`,
|
|
14
|
+
})
|
|
15
|
+
class TestHostComponent {
|
|
16
|
+
drawer = viewChild(SnyDrawerLayoutDirective);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe('SnyDrawerLayoutDirective', () => {
|
|
20
|
+
let fixture: ComponentFixture<TestHostComponent>;
|
|
21
|
+
|
|
22
|
+
beforeEach(async () => {
|
|
23
|
+
await TestBed.configureTestingModule({ imports: [TestHostComponent] }).compileComponents();
|
|
24
|
+
fixture = TestBed.createComponent(TestHostComponent);
|
|
25
|
+
fixture.detectChanges();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should be closed by default', () => {
|
|
29
|
+
const side = fixture.nativeElement.querySelector('[snyDrawerSide]');
|
|
30
|
+
expect(side.className).toContain('-translate-x-full');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should open on toggle', () => {
|
|
34
|
+
fixture.componentInstance.drawer()!.toggle();
|
|
35
|
+
fixture.detectChanges();
|
|
36
|
+
const side = fixture.nativeElement.querySelector('[snyDrawerSide]');
|
|
37
|
+
expect(side.className).toContain('translate-x-0');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should close after open', () => {
|
|
41
|
+
const d = fixture.componentInstance.drawer()!;
|
|
42
|
+
d.open();
|
|
43
|
+
fixture.detectChanges();
|
|
44
|
+
d.close();
|
|
45
|
+
fixture.detectChanges();
|
|
46
|
+
const side = fixture.nativeElement.querySelector('[snyDrawerSide]');
|
|
47
|
+
expect(side.className).toContain('-translate-x-full');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should have navigation role on drawer side', () => {
|
|
51
|
+
const side = fixture.nativeElement.querySelector('[snyDrawerSide]');
|
|
52
|
+
expect(side.getAttribute('role')).toBe('navigation');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should set aria-modal on side when overlay and open', () => {
|
|
56
|
+
const side = fixture.nativeElement.querySelector('[snyDrawerSide]');
|
|
57
|
+
expect(side.getAttribute('aria-modal')).toBeNull();
|
|
58
|
+
fixture.componentInstance.drawer()!.open();
|
|
59
|
+
fixture.detectChanges();
|
|
60
|
+
expect(side.getAttribute('aria-modal')).toBe('true');
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { ChangeDetectionStrategy, Component, Directive, InjectionToken, computed, inject, input, signal } from '@angular/core';
|
|
2
|
+
import { cn } from '../core/utils/cn';
|
|
3
|
+
|
|
4
|
+
export const SNY_DRAWER = new InjectionToken<SnyDrawerLayoutComponent>('SnyDrawer');
|
|
5
|
+
|
|
6
|
+
@Component({
|
|
7
|
+
selector: '[snyDrawerLayout]',
|
|
8
|
+
standalone: true,
|
|
9
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
10
|
+
exportAs: 'snyDrawerLayout',
|
|
11
|
+
providers: [{ provide: SNY_DRAWER, useExisting: SnyDrawerLayoutComponent }],
|
|
12
|
+
host: {
|
|
13
|
+
'[class]': 'computedClass()',
|
|
14
|
+
},
|
|
15
|
+
template: `
|
|
16
|
+
<ng-content />
|
|
17
|
+
@if (isOpen() && overlay()) {
|
|
18
|
+
<div
|
|
19
|
+
class="fixed inset-0 z-30 bg-black/50 transition-opacity"
|
|
20
|
+
(click)="close()"
|
|
21
|
+
></div>
|
|
22
|
+
}
|
|
23
|
+
`,
|
|
24
|
+
})
|
|
25
|
+
export class SnyDrawerLayoutComponent {
|
|
26
|
+
readonly class = input<string>('');
|
|
27
|
+
readonly overlay = input(true);
|
|
28
|
+
readonly isOpen = signal(false);
|
|
29
|
+
|
|
30
|
+
protected readonly computedClass = computed(() =>
|
|
31
|
+
cn('relative flex h-full w-full overflow-hidden', this.class())
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
toggle(): void { this.isOpen.update((v) => !v); }
|
|
35
|
+
open(): void { this.isOpen.set(true); }
|
|
36
|
+
close(): void { this.isOpen.set(false); }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** @deprecated Use SnyDrawerLayoutComponent instead */
|
|
40
|
+
export const SnyDrawerLayoutDirective = SnyDrawerLayoutComponent;
|
|
41
|
+
|
|
42
|
+
@Directive({
|
|
43
|
+
selector: '[snyDrawerContent]',
|
|
44
|
+
standalone: true,
|
|
45
|
+
host: {
|
|
46
|
+
'[class]': 'computedClass()',
|
|
47
|
+
},
|
|
48
|
+
})
|
|
49
|
+
export class SnyDrawerContentDirective {
|
|
50
|
+
readonly class = input<string>('');
|
|
51
|
+
protected readonly computedClass = computed(() =>
|
|
52
|
+
cn('flex-1 overflow-auto', this.class())
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export type DrawerSide = 'left' | 'right';
|
|
57
|
+
|
|
58
|
+
@Directive({
|
|
59
|
+
selector: '[snyDrawerSide]',
|
|
60
|
+
standalone: true,
|
|
61
|
+
host: {
|
|
62
|
+
'role': 'navigation',
|
|
63
|
+
'aria-label': 'Sidebar navigation',
|
|
64
|
+
'[attr.aria-modal]': 'drawer.overlay() && drawer.isOpen() || null',
|
|
65
|
+
'[class]': 'computedClass()',
|
|
66
|
+
},
|
|
67
|
+
})
|
|
68
|
+
export class SnyDrawerSideDirective {
|
|
69
|
+
protected readonly drawer = inject(SNY_DRAWER);
|
|
70
|
+
readonly side = input<DrawerSide>('left');
|
|
71
|
+
readonly class = input<string>('');
|
|
72
|
+
|
|
73
|
+
protected readonly computedClass = computed(() => {
|
|
74
|
+
const isOpen = this.drawer.isOpen();
|
|
75
|
+
const s = this.side();
|
|
76
|
+
const base = 'fixed inset-y-0 z-40 w-64 bg-background border-r border-border transition-transform duration-300 ease-in-out';
|
|
77
|
+
const sideClass = s === 'left' ? 'left-0' : 'right-0 border-l border-r-0';
|
|
78
|
+
const transformClass = isOpen
|
|
79
|
+
? 'translate-x-0'
|
|
80
|
+
: s === 'left' ? '-translate-x-full' : 'translate-x-full';
|
|
81
|
+
return cn(base, sideClass, transformClass, this.class());
|
|
82
|
+
});
|
|
83
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { Component, signal } from '@angular/core';
|
|
2
|
+
import { TestBed, type ComponentFixture } from '@angular/core/testing';
|
|
3
|
+
import {
|
|
4
|
+
SnyMenuContentDirective,
|
|
5
|
+
SnyMenuItemDirective,
|
|
6
|
+
SnyMenuSeparatorDirective,
|
|
7
|
+
SnyMenuLabelDirective,
|
|
8
|
+
} from './dropdown.directives';
|
|
9
|
+
import type { DropdownItemVariant } from './dropdown.variants';
|
|
10
|
+
|
|
11
|
+
@Component({
|
|
12
|
+
standalone: true,
|
|
13
|
+
imports: [
|
|
14
|
+
SnyMenuContentDirective,
|
|
15
|
+
SnyMenuItemDirective,
|
|
16
|
+
SnyMenuSeparatorDirective,
|
|
17
|
+
SnyMenuLabelDirective,
|
|
18
|
+
],
|
|
19
|
+
template: `
|
|
20
|
+
<div snyMenuContent>
|
|
21
|
+
<div snyMenuLabel>Actions</div>
|
|
22
|
+
<div snyMenuItem [variant]="variant()">Edit</div>
|
|
23
|
+
<div snyMenuSeparator></div>
|
|
24
|
+
<div snyMenuItem variant="destructive">Delete</div>
|
|
25
|
+
</div>
|
|
26
|
+
`,
|
|
27
|
+
})
|
|
28
|
+
class TestHostComponent {
|
|
29
|
+
variant = signal<DropdownItemVariant>('default');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe('SnyMenuContentDirective', () => {
|
|
33
|
+
let fixture: ComponentFixture<TestHostComponent>;
|
|
34
|
+
|
|
35
|
+
beforeEach(async () => {
|
|
36
|
+
await TestBed.configureTestingModule({
|
|
37
|
+
imports: [TestHostComponent],
|
|
38
|
+
}).compileComponents();
|
|
39
|
+
fixture = TestBed.createComponent(TestHostComponent);
|
|
40
|
+
fixture.detectChanges();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should apply content classes', () => {
|
|
44
|
+
const content = fixture.nativeElement.querySelector('[snyMenuContent]');
|
|
45
|
+
expect(content.className).toContain('rounded-md');
|
|
46
|
+
expect(content.className).toContain('bg-popover');
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('SnyMenuItemDirective', () => {
|
|
51
|
+
let fixture: ComponentFixture<TestHostComponent>;
|
|
52
|
+
|
|
53
|
+
beforeEach(async () => {
|
|
54
|
+
await TestBed.configureTestingModule({
|
|
55
|
+
imports: [TestHostComponent],
|
|
56
|
+
}).compileComponents();
|
|
57
|
+
fixture = TestBed.createComponent(TestHostComponent);
|
|
58
|
+
fixture.detectChanges();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should apply default item classes', () => {
|
|
62
|
+
const items = fixture.nativeElement.querySelectorAll('[snyMenuItem]');
|
|
63
|
+
expect(items[0].className).toContain('text-sm');
|
|
64
|
+
expect(items[0].className).toContain('rounded-sm');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should apply destructive variant', () => {
|
|
68
|
+
const items = fixture.nativeElement.querySelectorAll('[snyMenuItem]');
|
|
69
|
+
expect(items[1].className).toContain('text-destructive');
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe('SnyMenuSeparatorDirective', () => {
|
|
74
|
+
let fixture: ComponentFixture<TestHostComponent>;
|
|
75
|
+
|
|
76
|
+
beforeEach(async () => {
|
|
77
|
+
await TestBed.configureTestingModule({
|
|
78
|
+
imports: [TestHostComponent],
|
|
79
|
+
}).compileComponents();
|
|
80
|
+
fixture = TestBed.createComponent(TestHostComponent);
|
|
81
|
+
fixture.detectChanges();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should apply separator classes and role', () => {
|
|
85
|
+
const separator = fixture.nativeElement.querySelector('[snyMenuSeparator]');
|
|
86
|
+
expect(separator.getAttribute('role')).toBe('separator');
|
|
87
|
+
expect(separator.className).toContain('bg-muted');
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe('SnyMenuLabelDirective', () => {
|
|
92
|
+
let fixture: ComponentFixture<TestHostComponent>;
|
|
93
|
+
|
|
94
|
+
beforeEach(async () => {
|
|
95
|
+
await TestBed.configureTestingModule({
|
|
96
|
+
imports: [TestHostComponent],
|
|
97
|
+
}).compileComponents();
|
|
98
|
+
fixture = TestBed.createComponent(TestHostComponent);
|
|
99
|
+
fixture.detectChanges();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should apply label classes', () => {
|
|
103
|
+
const label = fixture.nativeElement.querySelector('[snyMenuLabel]');
|
|
104
|
+
expect(label.className).toContain('font-semibold');
|
|
105
|
+
});
|
|
106
|
+
});
|