@shadng/sng-ui 1.0.0
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/LICENSE +21 -0
- package/README.md +75 -0
- package/cli/sng-ui.js +331 -0
- package/ng-package.json +29 -0
- package/package.json +64 -0
- package/registry.json +72 -0
- package/src/lib/accordion/cn.ts +6 -0
- package/src/lib/accordion/index.ts +18 -0
- package/src/lib/accordion/sng-accordion-content.ts +131 -0
- package/src/lib/accordion/sng-accordion-item.ts +299 -0
- package/src/lib/accordion/sng-accordion-trigger.ts +137 -0
- package/src/lib/accordion/sng-accordion.ts +118 -0
- package/src/lib/accordion/sng-accordion.types.ts +82 -0
- package/src/lib/alert/cn.ts +6 -0
- package/src/lib/alert/index.ts +3 -0
- package/src/lib/alert/sng-alert-description.ts +49 -0
- package/src/lib/alert/sng-alert-title.ts +46 -0
- package/src/lib/alert/sng-alert.ts +48 -0
- package/src/lib/avatar/cn.ts +6 -0
- package/src/lib/avatar/index.ts +3 -0
- package/src/lib/avatar/sng-avatar-fallback.ts +50 -0
- package/src/lib/avatar/sng-avatar-image.ts +73 -0
- package/src/lib/avatar/sng-avatar.ts +60 -0
- package/src/lib/badge/cn.ts +6 -0
- package/src/lib/badge/index.ts +1 -0
- package/src/lib/badge/sng-badge.ts +36 -0
- package/src/lib/breadcrumb/cn.ts +6 -0
- package/src/lib/breadcrumb/index.ts +7 -0
- package/src/lib/breadcrumb/sng-breadcrumb-ellipsis.ts +61 -0
- package/src/lib/breadcrumb/sng-breadcrumb-item.ts +47 -0
- package/src/lib/breadcrumb/sng-breadcrumb-link.ts +43 -0
- package/src/lib/breadcrumb/sng-breadcrumb-list.ts +42 -0
- package/src/lib/breadcrumb/sng-breadcrumb-page.ts +44 -0
- package/src/lib/breadcrumb/sng-breadcrumb-separator.ts +60 -0
- package/src/lib/breadcrumb/sng-breadcrumb.ts +52 -0
- package/src/lib/button/cn.ts +6 -0
- package/src/lib/button/index.ts +2 -0
- package/src/lib/button/sng-button.ts +264 -0
- package/src/lib/calendar/cn.ts +6 -0
- package/src/lib/calendar/index.ts +2 -0
- package/src/lib/calendar/sng-calendar.ts +753 -0
- package/src/lib/card/cn.ts +6 -0
- package/src/lib/card/index.ts +6 -0
- package/src/lib/card/sng-card-content.ts +36 -0
- package/src/lib/card/sng-card-description.ts +38 -0
- package/src/lib/card/sng-card-footer.ts +34 -0
- package/src/lib/card/sng-card-header.ts +34 -0
- package/src/lib/card/sng-card-title.ts +48 -0
- package/src/lib/card/sng-card.ts +43 -0
- package/src/lib/carousel/cn.ts +6 -0
- package/src/lib/carousel/index.ts +18 -0
- package/src/lib/carousel/sng-carousel.ts +526 -0
- package/src/lib/checkbox/cn.ts +6 -0
- package/src/lib/checkbox/index.ts +1 -0
- package/src/lib/checkbox/sng-checkbox.ts +154 -0
- package/src/lib/code-block/cn.ts +6 -0
- package/src/lib/code-block/index.ts +1 -0
- package/src/lib/code-block/sng-code-block.ts +296 -0
- package/src/lib/dialog/cn.ts +6 -0
- package/src/lib/dialog/index.ts +37 -0
- package/src/lib/dialog/sng-dialog-close.ts +76 -0
- package/src/lib/dialog/sng-dialog-content.ts +132 -0
- package/src/lib/dialog/sng-dialog-description.ts +36 -0
- package/src/lib/dialog/sng-dialog-footer.ts +39 -0
- package/src/lib/dialog/sng-dialog-header.ts +39 -0
- package/src/lib/dialog/sng-dialog-title.ts +52 -0
- package/src/lib/dialog/sng-dialog.service.ts +222 -0
- package/src/lib/dialog/sng-dialog.ts +224 -0
- package/src/lib/drawer/cn.ts +6 -0
- package/src/lib/drawer/index.ts +36 -0
- package/src/lib/drawer/sng-drawer-close.ts +28 -0
- package/src/lib/drawer/sng-drawer-content.ts +135 -0
- package/src/lib/drawer/sng-drawer-description.ts +29 -0
- package/src/lib/drawer/sng-drawer-footer.ts +34 -0
- package/src/lib/drawer/sng-drawer-handle.ts +30 -0
- package/src/lib/drawer/sng-drawer-header.ts +30 -0
- package/src/lib/drawer/sng-drawer-title.ts +27 -0
- package/src/lib/drawer/sng-drawer-trigger.ts +21 -0
- package/src/lib/drawer/sng-drawer-wrapper.ts +27 -0
- package/src/lib/drawer/sng-drawer.ts +166 -0
- package/src/lib/file-input/cn.ts +6 -0
- package/src/lib/file-input/index.ts +1 -0
- package/src/lib/file-input/sng-file-input.ts +288 -0
- package/src/lib/hover-card/cn.ts +6 -0
- package/src/lib/hover-card/index.ts +3 -0
- package/src/lib/hover-card/sng-hover-card-content.ts +100 -0
- package/src/lib/hover-card/sng-hover-card-trigger.ts +43 -0
- package/src/lib/hover-card/sng-hover-card.ts +246 -0
- package/src/lib/input/cn.ts +6 -0
- package/src/lib/input/index.ts +1 -0
- package/src/lib/input/sng-input.ts +160 -0
- package/src/lib/layout/cn.ts +6 -0
- package/src/lib/layout/index.ts +98 -0
- package/src/lib/layout/sng-layout-footer.ts +37 -0
- package/src/lib/layout/sng-layout-header.ts +38 -0
- package/src/lib/layout/sng-layout-sidebar-content.ts +149 -0
- package/src/lib/layout/sng-layout-sidebar-footer.ts +54 -0
- package/src/lib/layout/sng-layout-sidebar-group-action.ts +67 -0
- package/src/lib/layout/sng-layout-sidebar-group-content.ts +41 -0
- package/src/lib/layout/sng-layout-sidebar-group-label.ts +53 -0
- package/src/lib/layout/sng-layout-sidebar-group.ts +41 -0
- package/src/lib/layout/sng-layout-sidebar-header.ts +54 -0
- package/src/lib/layout/sng-layout-sidebar-input.ts +112 -0
- package/src/lib/layout/sng-layout-sidebar-inset.ts +45 -0
- package/src/lib/layout/sng-layout-sidebar-menu-action.ts +84 -0
- package/src/lib/layout/sng-layout-sidebar-menu-badge.ts +47 -0
- package/src/lib/layout/sng-layout-sidebar-menu-button.ts +160 -0
- package/src/lib/layout/sng-layout-sidebar-menu-item.ts +40 -0
- package/src/lib/layout/sng-layout-sidebar-menu-skeleton.ts +71 -0
- package/src/lib/layout/sng-layout-sidebar-menu-sub-button.ts +142 -0
- package/src/lib/layout/sng-layout-sidebar-menu-sub-item.ts +38 -0
- package/src/lib/layout/sng-layout-sidebar-menu-sub.ts +48 -0
- package/src/lib/layout/sng-layout-sidebar-menu.ts +41 -0
- package/src/lib/layout/sng-layout-sidebar-provider.ts +189 -0
- package/src/lib/layout/sng-layout-sidebar-rail.ts +60 -0
- package/src/lib/layout/sng-layout-sidebar-separator.ts +38 -0
- package/src/lib/layout/sng-layout-sidebar-trigger.ts +97 -0
- package/src/lib/layout/sng-layout-sidebar.ts +254 -0
- package/src/lib/menu/cn.ts +6 -0
- package/src/lib/menu/index.ts +21 -0
- package/src/lib/menu/sng-context-trigger.ts +128 -0
- package/src/lib/menu/sng-menu-checkbox-item.ts +91 -0
- package/src/lib/menu/sng-menu-item.ts +80 -0
- package/src/lib/menu/sng-menu-label.ts +47 -0
- package/src/lib/menu/sng-menu-radio-group.ts +38 -0
- package/src/lib/menu/sng-menu-radio-item.ts +94 -0
- package/src/lib/menu/sng-menu-separator.ts +27 -0
- package/src/lib/menu/sng-menu-shortcut.ts +25 -0
- package/src/lib/menu/sng-menu-sub-content.ts +267 -0
- package/src/lib/menu/sng-menu-sub-trigger.ts +68 -0
- package/src/lib/menu/sng-menu-sub.ts +124 -0
- package/src/lib/menu/sng-menu-tokens.ts +52 -0
- package/src/lib/menu/sng-menu-trigger.ts +266 -0
- package/src/lib/menu/sng-menu.ts +100 -0
- package/src/lib/nav-menu/cn.ts +6 -0
- package/src/lib/nav-menu/index.ts +6 -0
- package/src/lib/nav-menu/sng-nav-menu-content.ts +72 -0
- package/src/lib/nav-menu/sng-nav-menu-item.ts +109 -0
- package/src/lib/nav-menu/sng-nav-menu-link.ts +54 -0
- package/src/lib/nav-menu/sng-nav-menu-list.ts +43 -0
- package/src/lib/nav-menu/sng-nav-menu-trigger.ts +98 -0
- package/src/lib/nav-menu/sng-nav-menu.ts +99 -0
- package/src/lib/otp-input/cn.ts +6 -0
- package/src/lib/otp-input/index.ts +14 -0
- package/src/lib/otp-input/sng-otp-input-group.ts +38 -0
- package/src/lib/otp-input/sng-otp-input-separator.ts +43 -0
- package/src/lib/otp-input/sng-otp-input-slot.ts +128 -0
- package/src/lib/otp-input/sng-otp-input-tokens.ts +20 -0
- package/src/lib/otp-input/sng-otp-input.ts +301 -0
- package/src/lib/popover/cn.ts +6 -0
- package/src/lib/popover/index.ts +3 -0
- package/src/lib/popover/sng-popover-content.ts +66 -0
- package/src/lib/popover/sng-popover-trigger.ts +44 -0
- package/src/lib/popover/sng-popover.ts +218 -0
- package/src/lib/preview-box/cn.ts +6 -0
- package/src/lib/preview-box/index.ts +5 -0
- package/src/lib/preview-box/sng-code-block.ts +80 -0
- package/src/lib/preview-box/sng-html-block.ts +79 -0
- package/src/lib/preview-box/sng-preview-block.ts +47 -0
- package/src/lib/preview-box/sng-preview-box.ts +369 -0
- package/src/lib/preview-box/sng-style-block.ts +80 -0
- package/src/lib/progress/cn.ts +6 -0
- package/src/lib/progress/index.ts +1 -0
- package/src/lib/progress/sng-progress.ts +65 -0
- package/src/lib/radio/cn.ts +6 -0
- package/src/lib/radio/index.ts +5 -0
- package/src/lib/radio/sng-radio-item.ts +100 -0
- package/src/lib/radio/sng-radio.ts +54 -0
- package/src/lib/resizable/cn.ts +6 -0
- package/src/lib/resizable/index.ts +3 -0
- package/src/lib/resizable/sng-resizable-group.ts +188 -0
- package/src/lib/resizable/sng-resizable-handle.ts +236 -0
- package/src/lib/resizable/sng-resizable-panel.ts +71 -0
- package/src/lib/search-input/cn.ts +6 -0
- package/src/lib/search-input/index.ts +16 -0
- package/src/lib/search-input/sng-search-input-context.ts +24 -0
- package/src/lib/search-input/sng-search-input-empty.ts +42 -0
- package/src/lib/search-input/sng-search-input-group.ts +69 -0
- package/src/lib/search-input/sng-search-input-item.ts +164 -0
- package/src/lib/search-input/sng-search-input-list.ts +34 -0
- package/src/lib/search-input/sng-search-input-separator.ts +32 -0
- package/src/lib/search-input/sng-search-input-shortcut.ts +29 -0
- package/src/lib/search-input/sng-search-input.ts +368 -0
- package/src/lib/select/cn.ts +6 -0
- package/src/lib/select/index.ts +7 -0
- package/src/lib/select/sng-select-content.ts +27 -0
- package/src/lib/select/sng-select-empty.ts +48 -0
- package/src/lib/select/sng-select-group.ts +29 -0
- package/src/lib/select/sng-select-item.ts +140 -0
- package/src/lib/select/sng-select-label.ts +29 -0
- package/src/lib/select/sng-select-separator.ts +29 -0
- package/src/lib/select/sng-select.ts +326 -0
- package/src/lib/separator/cn.ts +6 -0
- package/src/lib/separator/index.ts +1 -0
- package/src/lib/separator/sng-separator.ts +40 -0
- package/src/lib/skeleton/cn.ts +6 -0
- package/src/lib/skeleton/index.ts +1 -0
- package/src/lib/skeleton/sng-skeleton.ts +49 -0
- package/src/lib/slider/cn.ts +6 -0
- package/src/lib/slider/index.ts +2 -0
- package/src/lib/slider/sng-slider.ts +137 -0
- package/src/lib/sng-table/cn.ts +6 -0
- package/src/lib/sng-table/flex-render.ts +222 -0
- package/src/lib/sng-table/index.ts +85 -0
- package/src/lib/sng-table/sng-table-body.ts +59 -0
- package/src/lib/sng-table/sng-table-caption.ts +49 -0
- package/src/lib/sng-table/sng-table-cell.ts +62 -0
- package/src/lib/sng-table/sng-table-footer.ts +60 -0
- package/src/lib/sng-table/sng-table-head.ts +66 -0
- package/src/lib/sng-table/sng-table-header.ts +48 -0
- package/src/lib/sng-table/sng-table-pagination.ts +265 -0
- package/src/lib/sng-table/sng-table-row.ts +65 -0
- package/src/lib/sng-table/sng-table.ts +67 -0
- package/src/lib/sng-table-core/core/create-cell.ts +117 -0
- package/src/lib/sng-table-core/core/create-column.ts +266 -0
- package/src/lib/sng-table-core/core/create-header.ts +271 -0
- package/src/lib/sng-table-core/core/create-row.ts +293 -0
- package/src/lib/sng-table-core/core/create-table.ts +534 -0
- package/src/lib/sng-table-core/core/types.ts +1197 -0
- package/src/lib/sng-table-core/core/utils.ts +307 -0
- package/src/lib/sng-table-core/features/column-filtering.ts +376 -0
- package/src/lib/sng-table-core/features/column-ordering.ts +159 -0
- package/src/lib/sng-table-core/features/column-pinning.ts +219 -0
- package/src/lib/sng-table-core/features/column-sizing.ts +268 -0
- package/src/lib/sng-table-core/features/column-visibility.ts +128 -0
- package/src/lib/sng-table-core/features/faceting.ts +279 -0
- package/src/lib/sng-table-core/features/fuzzy-filtering.ts +188 -0
- package/src/lib/sng-table-core/features/global-filtering.ts +128 -0
- package/src/lib/sng-table-core/features/pagination.ts +179 -0
- package/src/lib/sng-table-core/features/row-expanding.ts +181 -0
- package/src/lib/sng-table-core/features/row-grouping.ts +235 -0
- package/src/lib/sng-table-core/features/row-pinning.ts +196 -0
- package/src/lib/sng-table-core/features/row-selection.ts +298 -0
- package/src/lib/sng-table-core/features/sorting.ts +425 -0
- package/src/lib/sng-table-core/features/virtualization.ts +298 -0
- package/src/lib/sng-table-core/index.ts +235 -0
- package/src/lib/sng-table-core/row-models/core-row-model.ts +256 -0
- package/src/lib/sng-table-core/row-models/expanded-row-model.ts +175 -0
- package/src/lib/sng-table-core/row-models/filtered-row-model.ts +307 -0
- package/src/lib/sng-table-core/row-models/grouped-row-model.ts +290 -0
- package/src/lib/sng-table-core/row-models/paginated-row-model.ts +135 -0
- package/src/lib/sng-table-core/row-models/sorted-row-model.ts +197 -0
- package/src/lib/styles/sng-themes.css +164 -0
- package/src/lib/switch/cn.ts +6 -0
- package/src/lib/switch/index.ts +1 -0
- package/src/lib/switch/sng-switch.ts +137 -0
- package/src/lib/tabs/cn.ts +6 -0
- package/src/lib/tabs/index.ts +4 -0
- package/src/lib/tabs/sng-tabs-content.ts +66 -0
- package/src/lib/tabs/sng-tabs-list.ts +55 -0
- package/src/lib/tabs/sng-tabs-trigger.ts +86 -0
- package/src/lib/tabs/sng-tabs.ts +83 -0
- package/src/lib/toast/cn.ts +6 -0
- package/src/lib/toast/index.ts +3 -0
- package/src/lib/toast/sng-toast.service.ts +258 -0
- package/src/lib/toast/sng-toast.ts +101 -0
- package/src/lib/toast/sng-toaster.ts +67 -0
- package/src/lib/toggle/cn.ts +6 -0
- package/src/lib/toggle/index.ts +6 -0
- package/src/lib/toggle/sng-toggle-group-item.ts +89 -0
- package/src/lib/toggle/sng-toggle-group.ts +85 -0
- package/src/lib/toggle/sng-toggle.ts +78 -0
- package/src/lib/toggle-group/index.ts +6 -0
- package/src/lib/tooltip/cn.ts +6 -0
- package/src/lib/tooltip/index.ts +5 -0
- package/src/lib/tooltip/sng-tooltip-content.ts +64 -0
- package/src/lib/tooltip/sng-tooltip.ts +216 -0
- package/src/public-api.ts +207 -0
- package/tsconfig.json +24 -0
- package/tsconfig.lib.json +17 -0
- package/tsconfig.lib.prod.json +11 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Component,
|
|
3
|
+
ChangeDetectionStrategy,
|
|
4
|
+
input,
|
|
5
|
+
computed,
|
|
6
|
+
inject,
|
|
7
|
+
} from '@angular/core';
|
|
8
|
+
import { SNG_TABS } from './sng-tabs';
|
|
9
|
+
import { cn } from './cn';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Content panel that is shown when the corresponding tab trigger is active.
|
|
13
|
+
* Must be placed inside an `sng-tabs` component.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```html
|
|
17
|
+
* <sng-tabs defaultValue="account">
|
|
18
|
+
* <sng-tabs-list>
|
|
19
|
+
* <sng-tabs-trigger value="account">Account</sng-tabs-trigger>
|
|
20
|
+
* <sng-tabs-trigger value="settings">Settings</sng-tabs-trigger>
|
|
21
|
+
* </sng-tabs-list>
|
|
22
|
+
* <sng-tabs-content value="account">
|
|
23
|
+
* <p>Manage your account details here.</p>
|
|
24
|
+
* </sng-tabs-content>
|
|
25
|
+
* <sng-tabs-content value="settings">
|
|
26
|
+
* <p>Configure your preferences.</p>
|
|
27
|
+
* </sng-tabs-content>
|
|
28
|
+
* </sng-tabs>
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
@Component({
|
|
32
|
+
selector: 'sng-tabs-content',
|
|
33
|
+
standalone: true,
|
|
34
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
35
|
+
host: {
|
|
36
|
+
'role': 'tabpanel',
|
|
37
|
+
'[class]': 'hostClasses()',
|
|
38
|
+
'[attr.id]': 'contentId()',
|
|
39
|
+
'[attr.aria-labelledby]': 'triggerId()',
|
|
40
|
+
'[attr.data-state]': 'isSelected() ? "active" : "inactive"',
|
|
41
|
+
'[hidden]': '!isSelected()',
|
|
42
|
+
},
|
|
43
|
+
template: `<ng-content />`,
|
|
44
|
+
})
|
|
45
|
+
export class SngTabsContent {
|
|
46
|
+
private tabs = inject(SNG_TABS);
|
|
47
|
+
|
|
48
|
+
/** Custom CSS classes. */
|
|
49
|
+
class = input<string>('');
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Unique identifier for this content panel. Must match the value of the corresponding `sng-tabs-trigger`.
|
|
53
|
+
*/
|
|
54
|
+
value = input.required<string>();
|
|
55
|
+
|
|
56
|
+
isSelected = computed(() => this.tabs.isSelected(this.value()));
|
|
57
|
+
contentId = computed(() => this.tabs.contentId(this.value()));
|
|
58
|
+
triggerId = computed(() => this.tabs.triggerId(this.value()));
|
|
59
|
+
|
|
60
|
+
hostClasses = computed(() =>
|
|
61
|
+
cn(
|
|
62
|
+
'flex-1 outline-none',
|
|
63
|
+
this.class()
|
|
64
|
+
)
|
|
65
|
+
);
|
|
66
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Component,
|
|
3
|
+
ChangeDetectionStrategy,
|
|
4
|
+
ViewEncapsulation,
|
|
5
|
+
input,
|
|
6
|
+
computed,
|
|
7
|
+
inject,
|
|
8
|
+
} from '@angular/core';
|
|
9
|
+
import { SNG_TABS } from './sng-tabs';
|
|
10
|
+
import { cn } from './cn';
|
|
11
|
+
|
|
12
|
+
// Style customization via [class]:
|
|
13
|
+
// Default (muted bg): class="bg-muted rounded-lg p-1" (built-in)
|
|
14
|
+
// Underline: class="border-b border-border bg-transparent p-0 rounded-none"
|
|
15
|
+
// Pills: class="bg-transparent gap-1 p-0"
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Container for tab triggers that provides proper tablist semantics.
|
|
19
|
+
* Must be placed inside an `sng-tabs` component.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```html
|
|
23
|
+
* <sng-tabs defaultValue="tab1">
|
|
24
|
+
* <sng-tabs-list>
|
|
25
|
+
* <sng-tabs-trigger value="tab1">Tab 1</sng-tabs-trigger>
|
|
26
|
+
* <sng-tabs-trigger value="tab2">Tab 2</sng-tabs-trigger>
|
|
27
|
+
* </sng-tabs-list>
|
|
28
|
+
* </sng-tabs>
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
@Component({
|
|
32
|
+
selector: 'sng-tabs-list',
|
|
33
|
+
standalone: true,
|
|
34
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
35
|
+
encapsulation: ViewEncapsulation.None,
|
|
36
|
+
host: {
|
|
37
|
+
'role': 'tablist',
|
|
38
|
+
'[class]': 'hostClasses()',
|
|
39
|
+
},
|
|
40
|
+
template: `<ng-content />`,
|
|
41
|
+
})
|
|
42
|
+
export class SngTabsList {
|
|
43
|
+
/** @internal */
|
|
44
|
+
readonly tabs = inject(SNG_TABS);
|
|
45
|
+
|
|
46
|
+
/** Custom CSS classes. */
|
|
47
|
+
class = input<string>('');
|
|
48
|
+
|
|
49
|
+
hostClasses = computed(() =>
|
|
50
|
+
cn(
|
|
51
|
+
'inline-flex items-center justify-center bg-muted rounded-lg p-1',
|
|
52
|
+
this.class()
|
|
53
|
+
)
|
|
54
|
+
);
|
|
55
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Component,
|
|
3
|
+
ChangeDetectionStrategy,
|
|
4
|
+
ElementRef,
|
|
5
|
+
input,
|
|
6
|
+
computed,
|
|
7
|
+
inject,
|
|
8
|
+
} from '@angular/core';
|
|
9
|
+
import { SNG_TABS } from './sng-tabs';
|
|
10
|
+
import { cn } from './cn';
|
|
11
|
+
|
|
12
|
+
const baseClasses = 'inline-flex items-center justify-center whitespace-nowrap px-3 py-1.5 text-sm font-medium transition-colors focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50 cursor-pointer select-none';
|
|
13
|
+
|
|
14
|
+
// Style customization via [class] + data-state attribute:
|
|
15
|
+
// Default (muted bg): (built-in) — active: bg-background text-foreground shadow-sm, inactive: text-muted-foreground
|
|
16
|
+
// Underline: class="border-b-2 -mb-px pb-1 px-0 py-0 rounded-none border-transparent data-[state=active]:border-foreground data-[state=active]:text-foreground"
|
|
17
|
+
// Pills: class="rounded-full data-[state=active]:bg-primary data-[state=active]:text-primary-foreground data-[state=inactive]:hover:bg-muted"
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A clickable tab trigger that activates the corresponding content panel.
|
|
21
|
+
* Must be placed inside an `sng-tabs-list` component.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```html
|
|
25
|
+
* <sng-tabs-list>
|
|
26
|
+
* <sng-tabs-trigger value="overview">Overview</sng-tabs-trigger>
|
|
27
|
+
* <sng-tabs-trigger value="analytics">Analytics</sng-tabs-trigger>
|
|
28
|
+
* <sng-tabs-trigger value="reports">Reports</sng-tabs-trigger>
|
|
29
|
+
* </sng-tabs-list>
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
@Component({
|
|
33
|
+
selector: 'sng-tabs-trigger',
|
|
34
|
+
standalone: true,
|
|
35
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
36
|
+
host: {
|
|
37
|
+
'role': 'tab',
|
|
38
|
+
'[class]': 'hostClasses()',
|
|
39
|
+
'[attr.data-state]': 'isSelected() ? "active" : "inactive"',
|
|
40
|
+
'[attr.data-value]': 'value()',
|
|
41
|
+
'[attr.id]': 'triggerId()',
|
|
42
|
+
'[attr.aria-controls]': 'contentId()',
|
|
43
|
+
'[attr.tabindex]': '0',
|
|
44
|
+
'[attr.aria-selected]': 'isSelected()',
|
|
45
|
+
'(click)': 'onClick()',
|
|
46
|
+
},
|
|
47
|
+
template: `<ng-content />`,
|
|
48
|
+
})
|
|
49
|
+
export class SngTabsTrigger {
|
|
50
|
+
private tabs = inject(SNG_TABS);
|
|
51
|
+
/** @internal */
|
|
52
|
+
_elementRef = inject(ElementRef);
|
|
53
|
+
|
|
54
|
+
/** Custom CSS classes. */
|
|
55
|
+
class = input<string>('');
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Unique identifier for this tab. Must match the value of the corresponding `sng-tabs-content`.
|
|
59
|
+
*/
|
|
60
|
+
value = input.required<string>();
|
|
61
|
+
|
|
62
|
+
isSelected = computed(() => this.tabs.isSelected(this.value()));
|
|
63
|
+
triggerId = computed(() => this.tabs.triggerId(this.value()));
|
|
64
|
+
contentId = computed(() => this.tabs.contentId(this.value()));
|
|
65
|
+
|
|
66
|
+
hostClasses = computed(() => {
|
|
67
|
+
const selected = this.isSelected();
|
|
68
|
+
return cn(
|
|
69
|
+
baseClasses,
|
|
70
|
+
'rounded-md',
|
|
71
|
+
selected
|
|
72
|
+
? 'bg-background text-foreground shadow-sm'
|
|
73
|
+
: 'text-muted-foreground hover:text-foreground',
|
|
74
|
+
this.class()
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
onClick() {
|
|
79
|
+
this.tabs.select(this.value());
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** @internal Focus this trigger element. */
|
|
83
|
+
_focus() {
|
|
84
|
+
this._elementRef.nativeElement.focus();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Component,
|
|
3
|
+
ChangeDetectionStrategy,
|
|
4
|
+
signal,
|
|
5
|
+
input,
|
|
6
|
+
output,
|
|
7
|
+
InjectionToken,
|
|
8
|
+
computed,
|
|
9
|
+
} from '@angular/core';
|
|
10
|
+
import { cn } from './cn';
|
|
11
|
+
|
|
12
|
+
export const SNG_TABS = new InjectionToken<SngTabs>('SNG_TABS');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Container component for a tabbed interface.
|
|
16
|
+
* Manages tab selection state and coordinates between triggers and content panels.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```html
|
|
20
|
+
* <sng-tabs defaultValue="account">
|
|
21
|
+
* <sng-tabs-list>
|
|
22
|
+
* <sng-tabs-trigger value="account">Account</sng-tabs-trigger>
|
|
23
|
+
* <sng-tabs-trigger value="password">Password</sng-tabs-trigger>
|
|
24
|
+
* </sng-tabs-list>
|
|
25
|
+
* <sng-tabs-content value="account">Account settings here.</sng-tabs-content>
|
|
26
|
+
* <sng-tabs-content value="password">Password settings here.</sng-tabs-content>
|
|
27
|
+
* </sng-tabs>
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
@Component({
|
|
31
|
+
selector: 'sng-tabs',
|
|
32
|
+
standalone: true,
|
|
33
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
34
|
+
providers: [{ provide: SNG_TABS, useExisting: SngTabs }],
|
|
35
|
+
host: {
|
|
36
|
+
'[class]': 'hostClasses()',
|
|
37
|
+
},
|
|
38
|
+
template: `<ng-content />`,
|
|
39
|
+
})
|
|
40
|
+
export class SngTabs {
|
|
41
|
+
private static _instanceCounter = 0;
|
|
42
|
+
private readonly _instanceId = `sng-tabs-${++SngTabs._instanceCounter}`;
|
|
43
|
+
|
|
44
|
+
/** Custom CSS classes. */
|
|
45
|
+
class = input<string>('');
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The value of the tab that should be selected by default.
|
|
49
|
+
*/
|
|
50
|
+
defaultValue = input<string>('');
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Emitted when the selected tab changes.
|
|
54
|
+
*/
|
|
55
|
+
valueChange = output<string>();
|
|
56
|
+
|
|
57
|
+
private _selectedValue = signal<string | null>(null);
|
|
58
|
+
|
|
59
|
+
hostClasses = computed(() => cn('flex flex-col gap-1', this.class()));
|
|
60
|
+
|
|
61
|
+
selectedValue = computed(() => this._selectedValue() ?? this.defaultValue());
|
|
62
|
+
|
|
63
|
+
select(value: string) {
|
|
64
|
+
this._selectedValue.set(value);
|
|
65
|
+
this.valueChange.emit(value);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
isSelected(value: string): boolean {
|
|
69
|
+
return this.selectedValue() === value;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
triggerId(value: string): string {
|
|
73
|
+
return `${this._instanceId}-trigger-${this.slug(value)}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
contentId(value: string): string {
|
|
77
|
+
return `${this._instanceId}-content-${this.slug(value)}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private slug(value: string): string {
|
|
81
|
+
return value.trim().toLowerCase().replace(/[^a-z0-9_-]+/g, '-');
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { Injectable, signal, Signal, inject } from '@angular/core';
|
|
2
|
+
import { LiveAnnouncer } from '@angular/cdk/a11y';
|
|
3
|
+
import { Subscription, timer } from 'rxjs';
|
|
4
|
+
|
|
5
|
+
export interface ToastAction {
|
|
6
|
+
label: string;
|
|
7
|
+
onClick: () => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type ToastPosition = 'top-left' | 'top-right' | 'top-center' | 'bottom-left' | 'bottom-right' | 'bottom-center';
|
|
11
|
+
export type ToastDismissType = 'countdown' | 'fixed';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Screen reader announcement priority for toast notifications.
|
|
15
|
+
* - 'polite': Waits for silence (default, use for success/info)
|
|
16
|
+
* - 'assertive': Interrupts immediately (use for errors)
|
|
17
|
+
*/
|
|
18
|
+
export type ToastPriority = 'polite' | 'assertive';
|
|
19
|
+
|
|
20
|
+
export interface Toast {
|
|
21
|
+
id: string;
|
|
22
|
+
title: string;
|
|
23
|
+
description?: string;
|
|
24
|
+
action?: ToastAction;
|
|
25
|
+
/**
|
|
26
|
+
* Custom Tailwind classes for styling. No variant input - use classes directly.
|
|
27
|
+
*
|
|
28
|
+
* Common styling patterns:
|
|
29
|
+
* - Success: `class="border-green-500 text-green-600"`
|
|
30
|
+
* - Error: `class="border-red-500 text-red-600"`
|
|
31
|
+
* - Warning: `class="border-yellow-500 text-yellow-600"`
|
|
32
|
+
* - Info: `class="border-blue-500 text-blue-600"`
|
|
33
|
+
* - Custom width: `class="w-[400px]"` (default: w-[360px])
|
|
34
|
+
*/
|
|
35
|
+
class?: string;
|
|
36
|
+
/** Screen reader announcement priority. */
|
|
37
|
+
priority?: ToastPriority;
|
|
38
|
+
duration?: number;
|
|
39
|
+
position?: ToastPosition;
|
|
40
|
+
dismissType?: ToastDismissType;
|
|
41
|
+
/** @internal Animation state for enter/exit transitions. */
|
|
42
|
+
_state?: 'open' | 'closed';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface ToastOptions {
|
|
46
|
+
title: string;
|
|
47
|
+
description?: string;
|
|
48
|
+
action?: ToastAction;
|
|
49
|
+
/**
|
|
50
|
+
* Custom Tailwind classes for styling. No variant input - use classes directly.
|
|
51
|
+
*
|
|
52
|
+
* Common styling patterns:
|
|
53
|
+
* - Success: `class="border-green-500 text-green-600"`
|
|
54
|
+
* - Error: `class="border-red-500 text-red-600"`
|
|
55
|
+
* - Warning: `class="border-yellow-500 text-yellow-600"`
|
|
56
|
+
* - Info: `class="border-blue-500 text-blue-600"`
|
|
57
|
+
* - Custom width: `class="w-[400px]"` (default: w-[360px])
|
|
58
|
+
*/
|
|
59
|
+
class?: string;
|
|
60
|
+
/** Screen reader announcement priority. */
|
|
61
|
+
priority?: ToastPriority;
|
|
62
|
+
duration?: number;
|
|
63
|
+
position?: ToastPosition;
|
|
64
|
+
dismissType?: ToastDismissType;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const TOASTS = signal<Toast[]>([]);
|
|
68
|
+
let nextToastId = 0;
|
|
69
|
+
|
|
70
|
+
function getToastsSignal() {
|
|
71
|
+
return TOASTS;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const MAX_TOASTS = 5;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Service for displaying toast notifications. Provides methods for showing
|
|
78
|
+
* different types of toasts (success, error, warning) with customizable
|
|
79
|
+
* position, duration, and actions. Automatically announces messages to
|
|
80
|
+
* screen readers for accessibility.
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* ```typescript
|
|
84
|
+
* export class MyComponent {
|
|
85
|
+
* private toastService = inject(SngToastService);
|
|
86
|
+
*
|
|
87
|
+
* showSuccess(): void {
|
|
88
|
+
* this.toastService.success('Changes saved', 'Your settings have been updated.');
|
|
89
|
+
* }
|
|
90
|
+
*
|
|
91
|
+
* showWithAction(): void {
|
|
92
|
+
* this.toastService.show({
|
|
93
|
+
* title: 'File deleted',
|
|
94
|
+
* description: 'The file has been moved to trash.',
|
|
95
|
+
* action: { label: 'Undo', onClick: () => this.undoDelete() },
|
|
96
|
+
* });
|
|
97
|
+
* }
|
|
98
|
+
* }
|
|
99
|
+
* ```
|
|
100
|
+
*/
|
|
101
|
+
@Injectable({ providedIn: 'root' })
|
|
102
|
+
export class SngToastService {
|
|
103
|
+
private liveAnnouncer = inject(LiveAnnouncer);
|
|
104
|
+
private autoDismissSubscriptions = new Map<string, Subscription>();
|
|
105
|
+
private removalSubscriptions = new Map<string, Subscription>();
|
|
106
|
+
|
|
107
|
+
/** Readonly signal containing all currently visible toasts. */
|
|
108
|
+
readonly toasts: Signal<readonly Toast[]> = getToastsSignal().asReadonly();
|
|
109
|
+
|
|
110
|
+
private generateId(): string {
|
|
111
|
+
nextToastId += 1;
|
|
112
|
+
return `toast-${nextToastId}`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Shows a toast notification with the provided options.
|
|
117
|
+
* @param options - Configuration for the toast including title, description, class, etc.
|
|
118
|
+
* @returns The unique ID of the created toast (can be used with dismiss()).
|
|
119
|
+
*/
|
|
120
|
+
show(options: ToastOptions): string {
|
|
121
|
+
const id = this.generateId();
|
|
122
|
+
const dismissType = options.dismissType ?? 'countdown';
|
|
123
|
+
const toast: Toast = {
|
|
124
|
+
id,
|
|
125
|
+
title: options.title,
|
|
126
|
+
description: options.description,
|
|
127
|
+
action: options.action,
|
|
128
|
+
class: options.class,
|
|
129
|
+
priority: options.priority ?? 'polite',
|
|
130
|
+
duration: options.duration ?? 3000,
|
|
131
|
+
position: options.position ?? 'bottom-right',
|
|
132
|
+
dismissType,
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
getToastsSignal().update(toasts => {
|
|
136
|
+
const newToasts = [...toasts, toast];
|
|
137
|
+
// Remove oldest toasts if exceeding max limit
|
|
138
|
+
if (newToasts.length > MAX_TOASTS) {
|
|
139
|
+
const removedToasts = newToasts.slice(0, newToasts.length - MAX_TOASTS);
|
|
140
|
+
removedToasts.forEach(removed => this.clearToastTimers(removed.id));
|
|
141
|
+
return newToasts.slice(-MAX_TOASTS);
|
|
142
|
+
}
|
|
143
|
+
return newToasts;
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// Announce to screen readers
|
|
147
|
+
const message = options.description
|
|
148
|
+
? `${options.title}. ${options.description}`
|
|
149
|
+
: options.title;
|
|
150
|
+
this.liveAnnouncer.announce(message, toast.priority!);
|
|
151
|
+
|
|
152
|
+
// Only auto-dismiss for countdown type
|
|
153
|
+
if (dismissType === 'countdown' && toast.duration && toast.duration > 0) {
|
|
154
|
+
const subscription = timer(toast.duration).subscribe(() => {
|
|
155
|
+
this.autoDismissSubscriptions.delete(id);
|
|
156
|
+
this.dismiss(id);
|
|
157
|
+
});
|
|
158
|
+
this.autoDismissSubscriptions.set(id, subscription);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return id;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Shows a success toast notification with green styling.
|
|
166
|
+
* Uses polite screen reader announcement.
|
|
167
|
+
* @param title - The toast title.
|
|
168
|
+
* @param description - Optional description text.
|
|
169
|
+
* @returns The unique ID of the created toast.
|
|
170
|
+
*/
|
|
171
|
+
success(title: string, description?: string): string {
|
|
172
|
+
return this.show({
|
|
173
|
+
title,
|
|
174
|
+
description,
|
|
175
|
+
class: 'border-green-500 text-green-600',
|
|
176
|
+
priority: 'polite',
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Shows an error toast notification with red styling.
|
|
182
|
+
* Uses assertive screen reader announcement (interrupts immediately).
|
|
183
|
+
* @param title - The toast title.
|
|
184
|
+
* @param description - Optional description text.
|
|
185
|
+
* @returns The unique ID of the created toast.
|
|
186
|
+
*/
|
|
187
|
+
error(title: string, description?: string): string {
|
|
188
|
+
return this.show({
|
|
189
|
+
title,
|
|
190
|
+
description,
|
|
191
|
+
class: 'border-red-500 text-red-600',
|
|
192
|
+
priority: 'assertive',
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Shows a warning toast notification with yellow styling.
|
|
198
|
+
* Uses polite screen reader announcement.
|
|
199
|
+
* @param title - The toast title.
|
|
200
|
+
* @param description - Optional description text.
|
|
201
|
+
* @returns The unique ID of the created toast.
|
|
202
|
+
*/
|
|
203
|
+
warning(title: string, description?: string): string {
|
|
204
|
+
return this.show({
|
|
205
|
+
title,
|
|
206
|
+
description,
|
|
207
|
+
class: 'border-yellow-500 text-yellow-600',
|
|
208
|
+
priority: 'polite',
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Dismisses a specific toast by its ID with an exit animation.
|
|
214
|
+
* Sets the toast state to 'closed' to trigger the CSS exit animation,
|
|
215
|
+
* then removes the toast from the array after the animation completes.
|
|
216
|
+
* @param id - The unique ID of the toast to dismiss.
|
|
217
|
+
*/
|
|
218
|
+
dismiss(id: string): void {
|
|
219
|
+
this.autoDismissSubscriptions.get(id)?.unsubscribe();
|
|
220
|
+
this.autoDismissSubscriptions.delete(id);
|
|
221
|
+
|
|
222
|
+
const currentToasts = getToastsSignal();
|
|
223
|
+
const targetToast = currentToasts().find(t => t.id === id);
|
|
224
|
+
if (!targetToast) return;
|
|
225
|
+
|
|
226
|
+
if (this.removalSubscriptions.has(id)) {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Set state to 'closed' to trigger exit animation
|
|
231
|
+
currentToasts.update(toasts =>
|
|
232
|
+
toasts.map(t => t.id === id ? { ...t, _state: 'closed' as const } : t)
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
// Remove after exit animation completes (300ms matches sng-toast-exit duration)
|
|
236
|
+
const subscription = timer(300).subscribe(() => {
|
|
237
|
+
this.removalSubscriptions.delete(id);
|
|
238
|
+
currentToasts.update(toasts => toasts.filter(t => t.id !== id));
|
|
239
|
+
});
|
|
240
|
+
this.removalSubscriptions.set(id, subscription);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** Immediately dismisses all visible toasts without animation. */
|
|
244
|
+
dismissAll(): void {
|
|
245
|
+
this.autoDismissSubscriptions.forEach(subscription => subscription.unsubscribe());
|
|
246
|
+
this.autoDismissSubscriptions.clear();
|
|
247
|
+
this.removalSubscriptions.forEach(subscription => subscription.unsubscribe());
|
|
248
|
+
this.removalSubscriptions.clear();
|
|
249
|
+
getToastsSignal().set([]);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
private clearToastTimers(id: string): void {
|
|
253
|
+
this.autoDismissSubscriptions.get(id)?.unsubscribe();
|
|
254
|
+
this.autoDismissSubscriptions.delete(id);
|
|
255
|
+
this.removalSubscriptions.get(id)?.unsubscribe();
|
|
256
|
+
this.removalSubscriptions.delete(id);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Component,
|
|
3
|
+
ChangeDetectionStrategy,
|
|
4
|
+
ViewEncapsulation,
|
|
5
|
+
input,
|
|
6
|
+
output,
|
|
7
|
+
computed,
|
|
8
|
+
} from '@angular/core';
|
|
9
|
+
import type { Toast } from './sng-toast.service';
|
|
10
|
+
import { cn } from './cn';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Individual toast notification component that displays a message with optional
|
|
14
|
+
* action button and close functionality.
|
|
15
|
+
*
|
|
16
|
+
* Styling is controlled via the `class` property on the Toast object.
|
|
17
|
+
* See SngToastService for styling patterns.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```html
|
|
21
|
+
* <sng-toast [toast]="toastData" (dismissed)="onDismiss()" />
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
@Component({
|
|
25
|
+
selector: 'sng-toast',
|
|
26
|
+
standalone: true,
|
|
27
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
28
|
+
encapsulation: ViewEncapsulation.None,
|
|
29
|
+
host: {},
|
|
30
|
+
styles: [`
|
|
31
|
+
.sng-toast[data-state=open] {
|
|
32
|
+
animation: sng-toast-enter var(--sng-toast-duration, 300ms) var(--sng-toast-ease, ease) both;
|
|
33
|
+
}
|
|
34
|
+
.sng-toast[data-state=closed] {
|
|
35
|
+
animation: sng-toast-exit var(--sng-toast-duration, 300ms) var(--sng-toast-ease, ease) both;
|
|
36
|
+
}
|
|
37
|
+
@keyframes sng-toast-enter {
|
|
38
|
+
from { opacity: 0; transform: translateX(1rem); }
|
|
39
|
+
}
|
|
40
|
+
@keyframes sng-toast-exit {
|
|
41
|
+
to { opacity: 0; transform: translateX(1rem); }
|
|
42
|
+
}
|
|
43
|
+
`],
|
|
44
|
+
template: `
|
|
45
|
+
<div class="sng-toast" [attr.data-state]="toast()._state ?? 'open'" [class]="containerClasses()">
|
|
46
|
+
<div class="flex-1 min-w-0 space-y-1 pr-6">
|
|
47
|
+
<div class="text-sm font-semibold">{{ toast().title }}</div>
|
|
48
|
+
@if (toast().description) {
|
|
49
|
+
<div class="text-sm text-muted-foreground">{{ toast().description }}</div>
|
|
50
|
+
}
|
|
51
|
+
</div>
|
|
52
|
+
@if (toast().action) {
|
|
53
|
+
<button
|
|
54
|
+
type="button"
|
|
55
|
+
class="inline-flex h-8 shrink-0 items-center justify-center rounded-md bg-primary px-3 text-sm font-medium text-primary-foreground hover:bg-primary/90 mr-6"
|
|
56
|
+
(click)="onAction()"
|
|
57
|
+
>
|
|
58
|
+
{{ toast().action!.label }}
|
|
59
|
+
</button>
|
|
60
|
+
}
|
|
61
|
+
<button
|
|
62
|
+
type="button"
|
|
63
|
+
[attr.aria-label]="dismissAriaLabel()"
|
|
64
|
+
class="absolute right-2 top-2 rounded-md p-1 text-foreground/50 hover:text-foreground"
|
|
65
|
+
(click)="dismissed.emit()"
|
|
66
|
+
>
|
|
67
|
+
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
68
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
|
69
|
+
</svg>
|
|
70
|
+
</button>
|
|
71
|
+
</div>
|
|
72
|
+
`,
|
|
73
|
+
})
|
|
74
|
+
export class SngToast {
|
|
75
|
+
/** The toast data object containing title, description, class, and other options. */
|
|
76
|
+
toast = input.required<Toast>();
|
|
77
|
+
|
|
78
|
+
/** Emits when the toast is dismissed via the close button or action button. */
|
|
79
|
+
dismissed = output<void>();
|
|
80
|
+
|
|
81
|
+
/** Aria label for the dismiss button. */
|
|
82
|
+
dismissAriaLabel = input<string>('Dismiss notification');
|
|
83
|
+
|
|
84
|
+
containerClasses = computed(() => {
|
|
85
|
+
const toast = this.toast();
|
|
86
|
+
|
|
87
|
+
// Base styles (always applied) — animation is driven by data-state attribute
|
|
88
|
+
const base = 'group pointer-events-auto relative flex items-start justify-between gap-4 overflow-hidden rounded-lg border p-4 shadow-lg text-sm bg-background border-border';
|
|
89
|
+
|
|
90
|
+
// Custom class (user-provided styling, e.g., "border-green-500 text-green-600 w-[400px]")
|
|
91
|
+
// Default width is w-[360px] if not specified
|
|
92
|
+
const customClass = toast.class || 'w-[360px]';
|
|
93
|
+
|
|
94
|
+
return cn(base, customClass);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
onAction(): void {
|
|
98
|
+
this.toast().action?.onClick();
|
|
99
|
+
this.dismissed.emit();
|
|
100
|
+
}
|
|
101
|
+
}
|