@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,52 @@
1
+ import {
2
+ Component,
3
+ input,
4
+ computed,
5
+ ChangeDetectionStrategy,
6
+ ViewEncapsulation,
7
+ } from '@angular/core';
8
+ import { cn } from './cn';
9
+
10
+ /** Heading level for dialog title. */
11
+ export type SngDialogTitleLevel = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
12
+
13
+ /**
14
+ * Title component for dialog content.
15
+ * Renders a heading element to provide consistent dialog title styling.
16
+ *
17
+ * @example
18
+ * ```html
19
+ * <sng-dialog-title>Edit Profile</sng-dialog-title>
20
+ * <sng-dialog-title level="h3">Settings</sng-dialog-title>
21
+ * ```
22
+ */
23
+ @Component({
24
+ selector: 'sng-dialog-title',
25
+ standalone: true,
26
+ changeDetection: ChangeDetectionStrategy.OnPush,
27
+ encapsulation: ViewEncapsulation.None,
28
+ host: {
29
+ 'class': 'contents',
30
+ },
31
+ template: `
32
+ @switch (level()) {
33
+ @case ('h1') { <h1 [class]="titleClasses()"><ng-content /></h1> }
34
+ @case ('h3') { <h3 [class]="titleClasses()"><ng-content /></h3> }
35
+ @case ('h4') { <h4 [class]="titleClasses()"><ng-content /></h4> }
36
+ @case ('h5') { <h5 [class]="titleClasses()"><ng-content /></h5> }
37
+ @case ('h6') { <h6 [class]="titleClasses()"><ng-content /></h6> }
38
+ @default { <h2 [class]="titleClasses()"><ng-content /></h2> }
39
+ }
40
+ `,
41
+ })
42
+ export class SngDialogTitle {
43
+ /** Heading level. */
44
+ level = input<SngDialogTitleLevel>('h2');
45
+
46
+ /** Custom CSS classes. */
47
+ class = input<string>('');
48
+
49
+ titleClasses = computed(() =>
50
+ cn('text-lg leading-none font-semibold', this.class())
51
+ );
52
+ }
@@ -0,0 +1,222 @@
1
+ import {
2
+ Injectable,
3
+ Injector,
4
+ InjectionToken,
5
+ Type,
6
+ inject,
7
+ signal,
8
+ } from '@angular/core';
9
+ import { Overlay, OverlayRef, OverlayConfig } from '@angular/cdk/overlay';
10
+ import { ComponentPortal } from '@angular/cdk/portal';
11
+ import { Subject } from 'rxjs';
12
+ import { SNG_DIALOG_CLOSE, SNG_DIALOG_INSTANCE } from './sng-dialog';
13
+
14
+ /** Injection token for passing data to dialog components */
15
+ export const SNG_DIALOG_DATA = new InjectionToken<unknown>('SNG_DIALOG_DATA');
16
+
17
+ /** Configuration options for opening a dialog */
18
+ export interface SngDialogConfig<D = unknown> {
19
+ /** Data to pass to the dialog component (inject with SNG_DIALOG_DATA) */
20
+ data?: D;
21
+ /** Width of the dialog (e.g., '400px', '80vw') */
22
+ width?: string;
23
+ /** Max width of the dialog */
24
+ maxWidth?: string;
25
+ /** Height of the dialog */
26
+ height?: string;
27
+ /** Max height of the dialog */
28
+ maxHeight?: string;
29
+ /** Whether clicking the backdrop closes the dialog (default: true) */
30
+ disableClose?: boolean;
31
+ /** Custom panel class for styling */
32
+ panelClass?: string | string[];
33
+ /** Whether the dialog has a backdrop (default: true) */
34
+ hasBackdrop?: boolean;
35
+ /** Custom backdrop class */
36
+ backdropClass?: string;
37
+ }
38
+
39
+ /** Reference to an opened dialog */
40
+ export class SngDialogRef<T = unknown, R = unknown> {
41
+ private readonly _afterClosed = new Subject<R | undefined>();
42
+ private _result: R | undefined;
43
+ private _closing = false;
44
+
45
+ /** @internal Signal for SngDialogContent to track open state */
46
+ _isOpenSignal: ReturnType<typeof signal<boolean>> | null = null;
47
+
48
+ /** Observable that emits when the dialog is closed */
49
+ readonly afterClosed$ = this._afterClosed.asObservable();
50
+
51
+ constructor(
52
+ private overlayRef: OverlayRef,
53
+ /** The component instance inside the dialog */
54
+ public readonly componentInstance: T
55
+ ) { }
56
+
57
+ /**
58
+ * Closes the dialog with a CSS exit animation, then disposes the overlay.
59
+ *
60
+ * **Animation lifecycle only** — triggers the exit animation by setting
61
+ * `data-state="closed"` on overlay elements and waits for all CSS animations
62
+ * to finish (via Web Animations API) before removing the overlay.
63
+ *
64
+ * Do not add business logic here. Instead, use `afterClosed$` to react
65
+ * after the dialog is fully closed:
66
+ * ```typescript
67
+ * dialogRef.afterClosed$.subscribe(result => {
68
+ * if (result) this.saveData(result);
69
+ * });
70
+ * ```
71
+ *
72
+ * @param result Optional value to pass to `afterClosed$` subscribers.
73
+ */
74
+ close(result?: R): void {
75
+ if (this._closing) return;
76
+ this._closing = true;
77
+ this._result = result;
78
+
79
+ // Signal SngDialogContent that we're closing
80
+ this._isOpenSignal?.set(false);
81
+
82
+ // Set data-state="closed" directly on DOM to trigger CSS exit animations
83
+ const panel = this.overlayRef.overlayElement;
84
+ panel.querySelectorAll('[data-state]').forEach(el =>
85
+ el.setAttribute('data-state', 'closed')
86
+ );
87
+
88
+ const doDispose = () => {
89
+ this.overlayRef.dispose();
90
+ this._afterClosed.next(result);
91
+ this._afterClosed.complete();
92
+ };
93
+
94
+ // getAnimations() flushes styles, so newly triggered animations are returned.
95
+ const animations = panel.getAnimations({ subtree: true });
96
+ if (animations.length > 0) {
97
+ Promise.allSettled(animations.map(animation => animation.finished)).finally(doDispose);
98
+ } else {
99
+ doDispose();
100
+ }
101
+ }
102
+
103
+ /** Get the result (available after close) */
104
+ get result(): R | undefined {
105
+ return this._result;
106
+ }
107
+ }
108
+
109
+ @Injectable({ providedIn: 'root' })
110
+ export class SngDialogService {
111
+ private overlay = inject(Overlay);
112
+ private injector = inject(Injector);
113
+
114
+ /** Currently open dialogs (any type to allow heterogeneous dialog types) */
115
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
116
+ private openDialogs: SngDialogRef<any, any>[] = [];
117
+
118
+ /**
119
+ * Open a dialog with the given component
120
+ *
121
+ * @example
122
+ * ```typescript
123
+ * const dialogRef = this.dialogService.open(MyDialogComponent, {
124
+ * data: { name: 'World' },
125
+ * width: '400px'
126
+ * });
127
+ *
128
+ * dialogRef.afterClosed$.subscribe(result => {
129
+ * console.log('Dialog closed with:', result);
130
+ * });
131
+ * ```
132
+ */
133
+ open<T, D = unknown, R = unknown>(
134
+ component: Type<T>,
135
+ config: SngDialogConfig<D> = {}
136
+ ): SngDialogRef<T, R> {
137
+ const overlayRef = this.createOverlay(config);
138
+ const dialogRef = new SngDialogRef<T, R>(overlayRef, null as unknown as T);
139
+
140
+ // Create adapter for SNG_DIALOG_INSTANCE (used by SngDialogContent)
141
+ // Start as false so the closed→open transition triggers sng-dialog-enter animation
142
+ const isOpenSignal = signal(false);
143
+ dialogRef._isOpenSignal = isOpenSignal;
144
+ const dialogInstanceAdapter = {
145
+ isOpen: isOpenSignal,
146
+ close: () => dialogRef.close(),
147
+ alert: () => false,
148
+ };
149
+
150
+ // Create injector with dialog-specific providers
151
+ const dialogInjector = Injector.create({
152
+ providers: [
153
+ { provide: SNG_DIALOG_DATA, useValue: config.data },
154
+ { provide: SNG_DIALOG_CLOSE, useValue: () => dialogRef.close() },
155
+ { provide: SngDialogRef, useValue: dialogRef },
156
+ { provide: SNG_DIALOG_INSTANCE, useValue: dialogInstanceAdapter },
157
+ ],
158
+ parent: this.injector,
159
+ });
160
+
161
+ // Create and attach the component
162
+ const portal = new ComponentPortal(component, null, dialogInjector);
163
+ const componentRef = overlayRef.attach(portal);
164
+
165
+ // Set open after attach so SngDialogContent transitions from closed→open
166
+ isOpenSignal.set(true);
167
+
168
+ // Update dialogRef with actual component instance
169
+ (dialogRef as SngDialogRef<T, R> & { componentInstance: T }).componentInstance = componentRef.instance;
170
+
171
+ // Handle backdrop click
172
+ if (!config.disableClose) {
173
+ overlayRef.backdropClick().subscribe(() => dialogRef.close());
174
+ }
175
+
176
+ // Track open dialogs
177
+ this.openDialogs.push(dialogRef);
178
+ dialogRef.afterClosed$.subscribe(() => {
179
+ const index = this.openDialogs.indexOf(dialogRef);
180
+ if (index > -1) {
181
+ this.openDialogs.splice(index, 1);
182
+ }
183
+ });
184
+
185
+ return dialogRef;
186
+ }
187
+
188
+ /** Close all open dialogs */
189
+ closeAll(): void {
190
+ // Make a copy because close() modifies the openDialogs array
191
+ [...this.openDialogs].forEach((dialogRef) => dialogRef.close());
192
+ }
193
+
194
+ private createOverlay(config: SngDialogConfig): OverlayRef {
195
+ const overlayConfig = new OverlayConfig({
196
+ hasBackdrop: false, // SngDialogContent handles its own backdrop overlay
197
+ panelClass: this.getPanelClasses(config),
198
+ width: config.width,
199
+ maxWidth: config.maxWidth || '90vw',
200
+ height: config.height,
201
+ maxHeight: config.maxHeight || '90vh',
202
+ positionStrategy: this.overlay
203
+ .position()
204
+ .global(),
205
+ scrollStrategy: this.overlay.scrollStrategies.block(),
206
+ });
207
+
208
+ return this.overlay.create(overlayConfig);
209
+ }
210
+
211
+ private getPanelClasses(config: SngDialogConfig): string[] {
212
+ const classes = ['sng-dialog-panel'];
213
+ if (config.panelClass) {
214
+ if (Array.isArray(config.panelClass)) {
215
+ classes.push(...config.panelClass);
216
+ } else {
217
+ classes.push(config.panelClass);
218
+ }
219
+ }
220
+ return classes;
221
+ }
222
+ }
@@ -0,0 +1,224 @@
1
+ import {
2
+ Component,
3
+ signal,
4
+ input,
5
+ output,
6
+ ChangeDetectionStrategy,
7
+ TemplateRef,
8
+ ViewContainerRef,
9
+ inject,
10
+ Injector,
11
+ InjectionToken,
12
+ OnDestroy,
13
+ booleanAttribute,
14
+ Signal,
15
+ ElementRef,
16
+ } from '@angular/core';
17
+ import { Overlay, OverlayRef, OverlayConfig } from '@angular/cdk/overlay';
18
+ import { TemplatePortal } from '@angular/cdk/portal';
19
+ import { Subscription } from 'rxjs';
20
+ import { DOCUMENT } from '@angular/common';
21
+
22
+ export const SNG_DIALOG_CLOSE = new InjectionToken<() => void>('SNG_DIALOG_CLOSE');
23
+ export const SNG_DIALOG_INSTANCE = new InjectionToken<SngDialog>('SNG_DIALOG_INSTANCE');
24
+ export const SNG_DIALOG_STATE = new InjectionToken<Signal<boolean>>('SNG_DIALOG_STATE');
25
+
26
+ /**
27
+ * Modal dialog container using CDK Overlay.
28
+ * Manages dialog lifecycle including open/close animations.
29
+ *
30
+ * @example
31
+ * ```html
32
+ * <sng-dialog #dialog>
33
+ * <sng-button (click)="dialog.open(dialogContent)">Open Dialog</sng-button>
34
+ * <ng-template #dialogContent>
35
+ * <sng-dialog-content>
36
+ * <sng-dialog-header>
37
+ * <sng-dialog-title>Dialog Title</sng-dialog-title>
38
+ * <sng-dialog-description>Dialog description text.</sng-dialog-description>
39
+ * </sng-dialog-header>
40
+ * <sng-dialog-footer>
41
+ * <sng-button class="border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground" (click)="dialog.close()">Cancel</sng-button>
42
+ * <sng-button (click)="dialog.close()">Confirm</sng-button>
43
+ * </sng-dialog-footer>
44
+ * </sng-dialog-content>
45
+ * </ng-template>
46
+ * </sng-dialog>
47
+ * ```
48
+ */
49
+ @Component({
50
+ selector: 'sng-dialog',
51
+ standalone: true,
52
+ changeDetection: ChangeDetectionStrategy.OnPush,
53
+ host: {
54
+ 'class': 'contents',
55
+ },
56
+ template: `<ng-content />`,
57
+ })
58
+ export class SngDialog implements OnDestroy {
59
+ private overlay = inject(Overlay);
60
+ private viewContainerRef = inject(ViewContainerRef);
61
+ private hostElementRef = inject(ElementRef<HTMLElement>);
62
+ private document = inject(DOCUMENT);
63
+ private overlayRef: OverlayRef | null = null;
64
+ private subscriptions = new Subscription();
65
+ private _closing = false;
66
+ /** Element that had focus before dialog opened (for focus restoration in alert mode) */
67
+ private triggerElement: HTMLElement | null = null;
68
+ /** Active signal driving the current dialog instance (for mimicking service behavior) */
69
+ private _activeOpenSignal: ReturnType<typeof signal<boolean>> | null = null;
70
+
71
+ /**
72
+ * When true, behaves like an alert dialog:
73
+ * - Uses role="alertdialog" instead of role="dialog"
74
+ * - Does NOT close on backdrop click (requires explicit action)
75
+ * - Restores focus to trigger element on close
76
+ */
77
+ alert = input(false, { transform: booleanAttribute });
78
+
79
+ /**
80
+ * Emits when open state changes.
81
+ */
82
+ openChange = output<boolean>();
83
+
84
+ /** Whether the dialog is currently open. */
85
+ isOpen = signal(false);
86
+
87
+ /**
88
+ * Opens the dialog with the given template content.
89
+ * Creates a CDK Overlay, attaches the template, and triggers the CSS enter animation.
90
+ *
91
+ * This method is part of the **animation lifecycle** — it handles overlay creation
92
+ * and the open transition. Do not place business logic here.
93
+ * Use `openChange` output or template event handlers for user actions.
94
+ *
95
+ * @param template The template to render inside the overlay.
96
+ * @param triggerElement Optional element to restore focus to on close (alert mode).
97
+ */
98
+ open(template: TemplateRef<unknown>, triggerElement?: HTMLElement) {
99
+ if (this.overlayRef || this._closing) return;
100
+
101
+ // Store trigger for focus restoration in alert mode
102
+ if (this.alert()) {
103
+ this.triggerElement = this.resolveTriggerElement(triggerElement);
104
+ }
105
+
106
+ // Create a fresh signal for this specific dialog instance to ensure
107
+ // pristine state for the animation (mimicking SngDialogService behavior)
108
+ const localIsOpen = signal(false);
109
+ this._activeOpenSignal = localIsOpen;
110
+
111
+ const dialogAdapter = {
112
+ isOpen: localIsOpen,
113
+ close: () => this.close(),
114
+ alert: this.alert,
115
+ };
116
+
117
+ const config = new OverlayConfig({
118
+ hasBackdrop: false, // We handle overlay in sng-dialog-content
119
+ panelClass: this.alert() ? 'sng-alert-dialog-panel' : 'sng-dialog-panel',
120
+ positionStrategy: this.overlay.position()
121
+ .global(),
122
+ scrollStrategy: this.overlay.scrollStrategies.block(),
123
+ });
124
+
125
+ this.overlayRef = this.overlay.create(config);
126
+ const injector = Injector.create({
127
+ providers: [
128
+ { provide: SNG_DIALOG_CLOSE, useValue: () => this.close() },
129
+ { provide: SNG_DIALOG_INSTANCE, useValue: dialogAdapter },
130
+ // Use local signal for state token as well
131
+ { provide: SNG_DIALOG_STATE, useValue: localIsOpen },
132
+ ],
133
+ parent: this.viewContainerRef.injector,
134
+ });
135
+ const portal = new TemplatePortal(template, this.viewContainerRef, null, injector);
136
+ this.overlayRef.attach(portal);
137
+
138
+ // Sync local state with public state
139
+ localIsOpen.set(true);
140
+ this.isOpen.set(true);
141
+ this.openChange.emit(true);
142
+
143
+ // Overlay click is handled by sng-dialog-content's overlay div
144
+ }
145
+
146
+ /**
147
+ * Closes the dialog with a CSS exit animation, then disposes the overlay.
148
+ *
149
+ * **Animation lifecycle only** — this method triggers the exit animation by
150
+ * setting `data-state="closed"` on overlay elements and waits for all CSS
151
+ * animations to finish (via Web Animations API) before removing the overlay.
152
+ *
153
+ * Do not add business logic (API calls, navigation, data saving) here.
154
+ * Instead, use the `openChange` output or template event handlers:
155
+ * ```html
156
+ * <sng-button (click)="onSave(); dialog.close()">Save</sng-button>
157
+ * ```
158
+ * Or listen to `openChange`:
159
+ * ```html
160
+ * <sng-dialog (openChange)="onDialogStateChange($event)">
161
+ * ```
162
+ */
163
+ close() {
164
+ if (!this.overlayRef || this._closing) return;
165
+ this._closing = true;
166
+
167
+ this.isOpen.set(false);
168
+ this._activeOpenSignal?.set(false);
169
+ this.openChange.emit(false);
170
+
171
+ // Set data-state="closed" directly on DOM to trigger CSS exit animations
172
+ // immediately (Angular's signal binding will also set this on next CD — same value).
173
+ const panel = this.overlayRef.overlayElement;
174
+ panel.querySelectorAll('[data-state]').forEach(el =>
175
+ el.setAttribute('data-state', 'closed')
176
+ );
177
+
178
+ // getAnimations() flushes styles, so newly triggered animations are returned.
179
+ const animations = panel.getAnimations({ subtree: true });
180
+ if (animations.length > 0) {
181
+ Promise.allSettled(animations.map(animation => animation.finished)).finally(() => this.dispose());
182
+ } else {
183
+ this.dispose();
184
+ }
185
+ }
186
+
187
+ /** @internal Immediately removes the overlay from DOM. Called after exit animation completes. */
188
+ private dispose() {
189
+ this._closing = false;
190
+ this.subscriptions.unsubscribe();
191
+ this.subscriptions = new Subscription();
192
+ if (this.overlayRef) {
193
+ this.overlayRef.dispose();
194
+ this.overlayRef = null;
195
+ }
196
+ this.isOpen.set(false);
197
+
198
+ // Restore focus to trigger element in alert mode
199
+ if (this.alert() && this.triggerElement) {
200
+ this.triggerElement.focus();
201
+ this.triggerElement = null;
202
+ }
203
+ }
204
+
205
+ /** Resolves a reliable trigger element for alert-mode focus restoration. */
206
+ private resolveTriggerElement(explicitTrigger?: HTMLElement): HTMLElement | null {
207
+ if (explicitTrigger) return explicitTrigger;
208
+
209
+ const activeElement = this.document.activeElement;
210
+ if (activeElement instanceof HTMLElement && activeElement !== this.document.body) {
211
+ return activeElement;
212
+ }
213
+
214
+ const host = this.hostElementRef.nativeElement;
215
+ const fallbackTrigger = host.querySelector(
216
+ 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
217
+ ) as HTMLElement | null;
218
+ return fallbackTrigger ?? null;
219
+ }
220
+
221
+ ngOnDestroy() {
222
+ this.dispose();
223
+ }
224
+ }
@@ -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,36 @@
1
+ export * from './sng-drawer';
2
+ export * from './sng-drawer-trigger';
3
+ export * from './sng-drawer-content';
4
+ export * from './sng-drawer-header';
5
+ export * from './sng-drawer-footer';
6
+ export * from './sng-drawer-title';
7
+ export * from './sng-drawer-description';
8
+ export * from './sng-drawer-close';
9
+ export * from './sng-drawer-handle';
10
+ export * from './sng-drawer-wrapper';
11
+
12
+ // =============================================================================
13
+ // Sheet Backwards Compatibility Aliases (deprecated)
14
+ // =============================================================================
15
+ // Note: These are TypeScript-level aliases only. Template selectors have changed:
16
+ // - Old: <sng-sheet>, <sng-sheet-content>, etc.
17
+ // - New: <sng-drawer [modal]="true">, <sng-drawer-content>, etc.
18
+
19
+ /** @deprecated Use SngDrawer with [modal]="true" instead */
20
+ export { SngDrawer as SngSheet } from './sng-drawer';
21
+ /** @deprecated Use SngDrawerTrigger instead */
22
+ export { SngDrawerTrigger as SngSheetTrigger } from './sng-drawer-trigger';
23
+ /** @deprecated Use SngDrawerContent instead */
24
+ export { SngDrawerContent as SngSheetContent } from './sng-drawer-content';
25
+ /** @deprecated Use SngDrawerHeader instead */
26
+ export { SngDrawerHeader as SngSheetHeader } from './sng-drawer-header';
27
+ /** @deprecated Use SngDrawerTitle instead */
28
+ export { SngDrawerTitle as SngSheetTitle } from './sng-drawer-title';
29
+ /** @deprecated Use SngDrawerDescription instead */
30
+ export { SngDrawerDescription as SngSheetDescription } from './sng-drawer-description';
31
+ /** @deprecated Use SngDrawerFooter instead */
32
+ export { SngDrawerFooter as SngSheetFooter } from './sng-drawer-footer';
33
+ /** @deprecated Use SngDrawerClose instead */
34
+ export { SngDrawerClose as SngSheetClose } from './sng-drawer-close';
35
+ /** @deprecated Use SngDrawerHandle instead */
36
+ export { SngDrawerHandle as SngSheetHandle } from './sng-drawer-handle';
@@ -0,0 +1,28 @@
1
+ import { Directive, inject } from '@angular/core';
2
+ import { SNG_DRAWER_CLOSE } from './sng-drawer';
3
+
4
+ /**
5
+ * Directive that closes the drawer when clicked.
6
+ * Use as an element wrapper around any clickable content.
7
+ *
8
+ * @example
9
+ * ```html
10
+ * <sng-drawer-close>
11
+ * <sng-button class="bg-primary text-primary-foreground shadow-xs hover:bg-primary/90">Save</sng-button>
12
+ * </sng-drawer-close>
13
+ * ```
14
+ */
15
+ @Directive({
16
+ selector: 'sng-drawer-close',
17
+ standalone: true,
18
+ host: {
19
+ '(click)': 'onClick()',
20
+ },
21
+ })
22
+ export class SngDrawerClose {
23
+ private closeFn = inject(SNG_DRAWER_CLOSE, { optional: true });
24
+
25
+ onClick() {
26
+ this.closeFn?.();
27
+ }
28
+ }