@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.
Files changed (271) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +75 -0
  3. package/cli/sng-ui.js +331 -0
  4. package/ng-package.json +29 -0
  5. package/package.json +64 -0
  6. package/registry.json +72 -0
  7. package/src/lib/accordion/cn.ts +6 -0
  8. package/src/lib/accordion/index.ts +18 -0
  9. package/src/lib/accordion/sng-accordion-content.ts +131 -0
  10. package/src/lib/accordion/sng-accordion-item.ts +299 -0
  11. package/src/lib/accordion/sng-accordion-trigger.ts +137 -0
  12. package/src/lib/accordion/sng-accordion.ts +118 -0
  13. package/src/lib/accordion/sng-accordion.types.ts +82 -0
  14. package/src/lib/alert/cn.ts +6 -0
  15. package/src/lib/alert/index.ts +3 -0
  16. package/src/lib/alert/sng-alert-description.ts +49 -0
  17. package/src/lib/alert/sng-alert-title.ts +46 -0
  18. package/src/lib/alert/sng-alert.ts +48 -0
  19. package/src/lib/avatar/cn.ts +6 -0
  20. package/src/lib/avatar/index.ts +3 -0
  21. package/src/lib/avatar/sng-avatar-fallback.ts +50 -0
  22. package/src/lib/avatar/sng-avatar-image.ts +73 -0
  23. package/src/lib/avatar/sng-avatar.ts +60 -0
  24. package/src/lib/badge/cn.ts +6 -0
  25. package/src/lib/badge/index.ts +1 -0
  26. package/src/lib/badge/sng-badge.ts +36 -0
  27. package/src/lib/breadcrumb/cn.ts +6 -0
  28. package/src/lib/breadcrumb/index.ts +7 -0
  29. package/src/lib/breadcrumb/sng-breadcrumb-ellipsis.ts +61 -0
  30. package/src/lib/breadcrumb/sng-breadcrumb-item.ts +47 -0
  31. package/src/lib/breadcrumb/sng-breadcrumb-link.ts +43 -0
  32. package/src/lib/breadcrumb/sng-breadcrumb-list.ts +42 -0
  33. package/src/lib/breadcrumb/sng-breadcrumb-page.ts +44 -0
  34. package/src/lib/breadcrumb/sng-breadcrumb-separator.ts +60 -0
  35. package/src/lib/breadcrumb/sng-breadcrumb.ts +52 -0
  36. package/src/lib/button/cn.ts +6 -0
  37. package/src/lib/button/index.ts +2 -0
  38. package/src/lib/button/sng-button.ts +264 -0
  39. package/src/lib/calendar/cn.ts +6 -0
  40. package/src/lib/calendar/index.ts +2 -0
  41. package/src/lib/calendar/sng-calendar.ts +753 -0
  42. package/src/lib/card/cn.ts +6 -0
  43. package/src/lib/card/index.ts +6 -0
  44. package/src/lib/card/sng-card-content.ts +36 -0
  45. package/src/lib/card/sng-card-description.ts +38 -0
  46. package/src/lib/card/sng-card-footer.ts +34 -0
  47. package/src/lib/card/sng-card-header.ts +34 -0
  48. package/src/lib/card/sng-card-title.ts +48 -0
  49. package/src/lib/card/sng-card.ts +43 -0
  50. package/src/lib/carousel/cn.ts +6 -0
  51. package/src/lib/carousel/index.ts +18 -0
  52. package/src/lib/carousel/sng-carousel.ts +526 -0
  53. package/src/lib/checkbox/cn.ts +6 -0
  54. package/src/lib/checkbox/index.ts +1 -0
  55. package/src/lib/checkbox/sng-checkbox.ts +154 -0
  56. package/src/lib/code-block/cn.ts +6 -0
  57. package/src/lib/code-block/index.ts +1 -0
  58. package/src/lib/code-block/sng-code-block.ts +296 -0
  59. package/src/lib/dialog/cn.ts +6 -0
  60. package/src/lib/dialog/index.ts +37 -0
  61. package/src/lib/dialog/sng-dialog-close.ts +76 -0
  62. package/src/lib/dialog/sng-dialog-content.ts +132 -0
  63. package/src/lib/dialog/sng-dialog-description.ts +36 -0
  64. package/src/lib/dialog/sng-dialog-footer.ts +39 -0
  65. package/src/lib/dialog/sng-dialog-header.ts +39 -0
  66. package/src/lib/dialog/sng-dialog-title.ts +52 -0
  67. package/src/lib/dialog/sng-dialog.service.ts +222 -0
  68. package/src/lib/dialog/sng-dialog.ts +224 -0
  69. package/src/lib/drawer/cn.ts +6 -0
  70. package/src/lib/drawer/index.ts +36 -0
  71. package/src/lib/drawer/sng-drawer-close.ts +28 -0
  72. package/src/lib/drawer/sng-drawer-content.ts +135 -0
  73. package/src/lib/drawer/sng-drawer-description.ts +29 -0
  74. package/src/lib/drawer/sng-drawer-footer.ts +34 -0
  75. package/src/lib/drawer/sng-drawer-handle.ts +30 -0
  76. package/src/lib/drawer/sng-drawer-header.ts +30 -0
  77. package/src/lib/drawer/sng-drawer-title.ts +27 -0
  78. package/src/lib/drawer/sng-drawer-trigger.ts +21 -0
  79. package/src/lib/drawer/sng-drawer-wrapper.ts +27 -0
  80. package/src/lib/drawer/sng-drawer.ts +166 -0
  81. package/src/lib/file-input/cn.ts +6 -0
  82. package/src/lib/file-input/index.ts +1 -0
  83. package/src/lib/file-input/sng-file-input.ts +288 -0
  84. package/src/lib/hover-card/cn.ts +6 -0
  85. package/src/lib/hover-card/index.ts +3 -0
  86. package/src/lib/hover-card/sng-hover-card-content.ts +100 -0
  87. package/src/lib/hover-card/sng-hover-card-trigger.ts +43 -0
  88. package/src/lib/hover-card/sng-hover-card.ts +246 -0
  89. package/src/lib/input/cn.ts +6 -0
  90. package/src/lib/input/index.ts +1 -0
  91. package/src/lib/input/sng-input.ts +160 -0
  92. package/src/lib/layout/cn.ts +6 -0
  93. package/src/lib/layout/index.ts +98 -0
  94. package/src/lib/layout/sng-layout-footer.ts +37 -0
  95. package/src/lib/layout/sng-layout-header.ts +38 -0
  96. package/src/lib/layout/sng-layout-sidebar-content.ts +149 -0
  97. package/src/lib/layout/sng-layout-sidebar-footer.ts +54 -0
  98. package/src/lib/layout/sng-layout-sidebar-group-action.ts +67 -0
  99. package/src/lib/layout/sng-layout-sidebar-group-content.ts +41 -0
  100. package/src/lib/layout/sng-layout-sidebar-group-label.ts +53 -0
  101. package/src/lib/layout/sng-layout-sidebar-group.ts +41 -0
  102. package/src/lib/layout/sng-layout-sidebar-header.ts +54 -0
  103. package/src/lib/layout/sng-layout-sidebar-input.ts +112 -0
  104. package/src/lib/layout/sng-layout-sidebar-inset.ts +45 -0
  105. package/src/lib/layout/sng-layout-sidebar-menu-action.ts +84 -0
  106. package/src/lib/layout/sng-layout-sidebar-menu-badge.ts +47 -0
  107. package/src/lib/layout/sng-layout-sidebar-menu-button.ts +160 -0
  108. package/src/lib/layout/sng-layout-sidebar-menu-item.ts +40 -0
  109. package/src/lib/layout/sng-layout-sidebar-menu-skeleton.ts +71 -0
  110. package/src/lib/layout/sng-layout-sidebar-menu-sub-button.ts +142 -0
  111. package/src/lib/layout/sng-layout-sidebar-menu-sub-item.ts +38 -0
  112. package/src/lib/layout/sng-layout-sidebar-menu-sub.ts +48 -0
  113. package/src/lib/layout/sng-layout-sidebar-menu.ts +41 -0
  114. package/src/lib/layout/sng-layout-sidebar-provider.ts +189 -0
  115. package/src/lib/layout/sng-layout-sidebar-rail.ts +60 -0
  116. package/src/lib/layout/sng-layout-sidebar-separator.ts +38 -0
  117. package/src/lib/layout/sng-layout-sidebar-trigger.ts +97 -0
  118. package/src/lib/layout/sng-layout-sidebar.ts +254 -0
  119. package/src/lib/menu/cn.ts +6 -0
  120. package/src/lib/menu/index.ts +21 -0
  121. package/src/lib/menu/sng-context-trigger.ts +128 -0
  122. package/src/lib/menu/sng-menu-checkbox-item.ts +91 -0
  123. package/src/lib/menu/sng-menu-item.ts +80 -0
  124. package/src/lib/menu/sng-menu-label.ts +47 -0
  125. package/src/lib/menu/sng-menu-radio-group.ts +38 -0
  126. package/src/lib/menu/sng-menu-radio-item.ts +94 -0
  127. package/src/lib/menu/sng-menu-separator.ts +27 -0
  128. package/src/lib/menu/sng-menu-shortcut.ts +25 -0
  129. package/src/lib/menu/sng-menu-sub-content.ts +267 -0
  130. package/src/lib/menu/sng-menu-sub-trigger.ts +68 -0
  131. package/src/lib/menu/sng-menu-sub.ts +124 -0
  132. package/src/lib/menu/sng-menu-tokens.ts +52 -0
  133. package/src/lib/menu/sng-menu-trigger.ts +266 -0
  134. package/src/lib/menu/sng-menu.ts +100 -0
  135. package/src/lib/nav-menu/cn.ts +6 -0
  136. package/src/lib/nav-menu/index.ts +6 -0
  137. package/src/lib/nav-menu/sng-nav-menu-content.ts +72 -0
  138. package/src/lib/nav-menu/sng-nav-menu-item.ts +109 -0
  139. package/src/lib/nav-menu/sng-nav-menu-link.ts +54 -0
  140. package/src/lib/nav-menu/sng-nav-menu-list.ts +43 -0
  141. package/src/lib/nav-menu/sng-nav-menu-trigger.ts +98 -0
  142. package/src/lib/nav-menu/sng-nav-menu.ts +99 -0
  143. package/src/lib/otp-input/cn.ts +6 -0
  144. package/src/lib/otp-input/index.ts +14 -0
  145. package/src/lib/otp-input/sng-otp-input-group.ts +38 -0
  146. package/src/lib/otp-input/sng-otp-input-separator.ts +43 -0
  147. package/src/lib/otp-input/sng-otp-input-slot.ts +128 -0
  148. package/src/lib/otp-input/sng-otp-input-tokens.ts +20 -0
  149. package/src/lib/otp-input/sng-otp-input.ts +301 -0
  150. package/src/lib/popover/cn.ts +6 -0
  151. package/src/lib/popover/index.ts +3 -0
  152. package/src/lib/popover/sng-popover-content.ts +66 -0
  153. package/src/lib/popover/sng-popover-trigger.ts +44 -0
  154. package/src/lib/popover/sng-popover.ts +218 -0
  155. package/src/lib/preview-box/cn.ts +6 -0
  156. package/src/lib/preview-box/index.ts +5 -0
  157. package/src/lib/preview-box/sng-code-block.ts +80 -0
  158. package/src/lib/preview-box/sng-html-block.ts +79 -0
  159. package/src/lib/preview-box/sng-preview-block.ts +47 -0
  160. package/src/lib/preview-box/sng-preview-box.ts +369 -0
  161. package/src/lib/preview-box/sng-style-block.ts +80 -0
  162. package/src/lib/progress/cn.ts +6 -0
  163. package/src/lib/progress/index.ts +1 -0
  164. package/src/lib/progress/sng-progress.ts +65 -0
  165. package/src/lib/radio/cn.ts +6 -0
  166. package/src/lib/radio/index.ts +5 -0
  167. package/src/lib/radio/sng-radio-item.ts +100 -0
  168. package/src/lib/radio/sng-radio.ts +54 -0
  169. package/src/lib/resizable/cn.ts +6 -0
  170. package/src/lib/resizable/index.ts +3 -0
  171. package/src/lib/resizable/sng-resizable-group.ts +188 -0
  172. package/src/lib/resizable/sng-resizable-handle.ts +236 -0
  173. package/src/lib/resizable/sng-resizable-panel.ts +71 -0
  174. package/src/lib/search-input/cn.ts +6 -0
  175. package/src/lib/search-input/index.ts +16 -0
  176. package/src/lib/search-input/sng-search-input-context.ts +24 -0
  177. package/src/lib/search-input/sng-search-input-empty.ts +42 -0
  178. package/src/lib/search-input/sng-search-input-group.ts +69 -0
  179. package/src/lib/search-input/sng-search-input-item.ts +164 -0
  180. package/src/lib/search-input/sng-search-input-list.ts +34 -0
  181. package/src/lib/search-input/sng-search-input-separator.ts +32 -0
  182. package/src/lib/search-input/sng-search-input-shortcut.ts +29 -0
  183. package/src/lib/search-input/sng-search-input.ts +368 -0
  184. package/src/lib/select/cn.ts +6 -0
  185. package/src/lib/select/index.ts +7 -0
  186. package/src/lib/select/sng-select-content.ts +27 -0
  187. package/src/lib/select/sng-select-empty.ts +48 -0
  188. package/src/lib/select/sng-select-group.ts +29 -0
  189. package/src/lib/select/sng-select-item.ts +140 -0
  190. package/src/lib/select/sng-select-label.ts +29 -0
  191. package/src/lib/select/sng-select-separator.ts +29 -0
  192. package/src/lib/select/sng-select.ts +326 -0
  193. package/src/lib/separator/cn.ts +6 -0
  194. package/src/lib/separator/index.ts +1 -0
  195. package/src/lib/separator/sng-separator.ts +40 -0
  196. package/src/lib/skeleton/cn.ts +6 -0
  197. package/src/lib/skeleton/index.ts +1 -0
  198. package/src/lib/skeleton/sng-skeleton.ts +49 -0
  199. package/src/lib/slider/cn.ts +6 -0
  200. package/src/lib/slider/index.ts +2 -0
  201. package/src/lib/slider/sng-slider.ts +137 -0
  202. package/src/lib/sng-table/cn.ts +6 -0
  203. package/src/lib/sng-table/flex-render.ts +222 -0
  204. package/src/lib/sng-table/index.ts +85 -0
  205. package/src/lib/sng-table/sng-table-body.ts +59 -0
  206. package/src/lib/sng-table/sng-table-caption.ts +49 -0
  207. package/src/lib/sng-table/sng-table-cell.ts +62 -0
  208. package/src/lib/sng-table/sng-table-footer.ts +60 -0
  209. package/src/lib/sng-table/sng-table-head.ts +66 -0
  210. package/src/lib/sng-table/sng-table-header.ts +48 -0
  211. package/src/lib/sng-table/sng-table-pagination.ts +265 -0
  212. package/src/lib/sng-table/sng-table-row.ts +65 -0
  213. package/src/lib/sng-table/sng-table.ts +67 -0
  214. package/src/lib/sng-table-core/core/create-cell.ts +117 -0
  215. package/src/lib/sng-table-core/core/create-column.ts +266 -0
  216. package/src/lib/sng-table-core/core/create-header.ts +271 -0
  217. package/src/lib/sng-table-core/core/create-row.ts +293 -0
  218. package/src/lib/sng-table-core/core/create-table.ts +534 -0
  219. package/src/lib/sng-table-core/core/types.ts +1197 -0
  220. package/src/lib/sng-table-core/core/utils.ts +307 -0
  221. package/src/lib/sng-table-core/features/column-filtering.ts +376 -0
  222. package/src/lib/sng-table-core/features/column-ordering.ts +159 -0
  223. package/src/lib/sng-table-core/features/column-pinning.ts +219 -0
  224. package/src/lib/sng-table-core/features/column-sizing.ts +268 -0
  225. package/src/lib/sng-table-core/features/column-visibility.ts +128 -0
  226. package/src/lib/sng-table-core/features/faceting.ts +279 -0
  227. package/src/lib/sng-table-core/features/fuzzy-filtering.ts +188 -0
  228. package/src/lib/sng-table-core/features/global-filtering.ts +128 -0
  229. package/src/lib/sng-table-core/features/pagination.ts +179 -0
  230. package/src/lib/sng-table-core/features/row-expanding.ts +181 -0
  231. package/src/lib/sng-table-core/features/row-grouping.ts +235 -0
  232. package/src/lib/sng-table-core/features/row-pinning.ts +196 -0
  233. package/src/lib/sng-table-core/features/row-selection.ts +298 -0
  234. package/src/lib/sng-table-core/features/sorting.ts +425 -0
  235. package/src/lib/sng-table-core/features/virtualization.ts +298 -0
  236. package/src/lib/sng-table-core/index.ts +235 -0
  237. package/src/lib/sng-table-core/row-models/core-row-model.ts +256 -0
  238. package/src/lib/sng-table-core/row-models/expanded-row-model.ts +175 -0
  239. package/src/lib/sng-table-core/row-models/filtered-row-model.ts +307 -0
  240. package/src/lib/sng-table-core/row-models/grouped-row-model.ts +290 -0
  241. package/src/lib/sng-table-core/row-models/paginated-row-model.ts +135 -0
  242. package/src/lib/sng-table-core/row-models/sorted-row-model.ts +197 -0
  243. package/src/lib/styles/sng-themes.css +164 -0
  244. package/src/lib/switch/cn.ts +6 -0
  245. package/src/lib/switch/index.ts +1 -0
  246. package/src/lib/switch/sng-switch.ts +137 -0
  247. package/src/lib/tabs/cn.ts +6 -0
  248. package/src/lib/tabs/index.ts +4 -0
  249. package/src/lib/tabs/sng-tabs-content.ts +66 -0
  250. package/src/lib/tabs/sng-tabs-list.ts +55 -0
  251. package/src/lib/tabs/sng-tabs-trigger.ts +86 -0
  252. package/src/lib/tabs/sng-tabs.ts +83 -0
  253. package/src/lib/toast/cn.ts +6 -0
  254. package/src/lib/toast/index.ts +3 -0
  255. package/src/lib/toast/sng-toast.service.ts +258 -0
  256. package/src/lib/toast/sng-toast.ts +101 -0
  257. package/src/lib/toast/sng-toaster.ts +67 -0
  258. package/src/lib/toggle/cn.ts +6 -0
  259. package/src/lib/toggle/index.ts +6 -0
  260. package/src/lib/toggle/sng-toggle-group-item.ts +89 -0
  261. package/src/lib/toggle/sng-toggle-group.ts +85 -0
  262. package/src/lib/toggle/sng-toggle.ts +78 -0
  263. package/src/lib/toggle-group/index.ts +6 -0
  264. package/src/lib/tooltip/cn.ts +6 -0
  265. package/src/lib/tooltip/index.ts +5 -0
  266. package/src/lib/tooltip/sng-tooltip-content.ts +64 -0
  267. package/src/lib/tooltip/sng-tooltip.ts +216 -0
  268. package/src/public-api.ts +207 -0
  269. package/tsconfig.json +24 -0
  270. package/tsconfig.lib.json +17 -0
  271. 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,6 @@
1
+ import { clsx, type ClassValue } from 'clsx';
2
+ import { twMerge } from 'tailwind-merge';
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs));
6
+ }
@@ -0,0 +1,3 @@
1
+ export * from './sng-toast.service';
2
+ export * from './sng-toast';
3
+ export * from './sng-toaster';
@@ -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
+ }