@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,47 @@
1
+ import {
2
+ Component,
3
+ ChangeDetectionStrategy,
4
+ input,
5
+ computed,
6
+ booleanAttribute,
7
+ } from '@angular/core';
8
+ import { cn } from './cn';
9
+
10
+ /**
11
+ * Non-interactive label for grouping or describing menu sections.
12
+ *
13
+ * @example
14
+ * ```html
15
+ * <sng-menu>
16
+ * <sng-menu-label>Actions</sng-menu-label>
17
+ * <sng-menu-item>Edit</sng-menu-item>
18
+ * <sng-menu-item>Delete</sng-menu-item>
19
+ * </sng-menu>
20
+ * ```
21
+ */
22
+ @Component({
23
+ selector: 'sng-menu-label',
24
+ standalone: true,
25
+ changeDetection: ChangeDetectionStrategy.OnPush,
26
+ host: {
27
+ '[class]': 'hostClasses()',
28
+ },
29
+ template: `<ng-content />`,
30
+ })
31
+ export class SngMenuLabel {
32
+ /** Custom CSS classes. */
33
+ class = input<string>('');
34
+
35
+ /**
36
+ * Whether to add left padding for alignment with checkbox/radio items.
37
+ */
38
+ inset = input(false, { transform: booleanAttribute });
39
+
40
+ hostClasses = computed(() =>
41
+ cn(
42
+ 'px-2 py-1.5 text-sm font-semibold text-foreground',
43
+ this.inset() && 'pl-8',
44
+ this.class()
45
+ )
46
+ );
47
+ }
@@ -0,0 +1,38 @@
1
+ import {
2
+ Component,
3
+ ChangeDetectionStrategy,
4
+ model,
5
+ } from '@angular/core';
6
+
7
+ /**
8
+ * Container for radio menu items. Manages single-selection state.
9
+ *
10
+ * @example
11
+ * ```html
12
+ * <sng-menu>
13
+ * <sng-menu-radio-group [(value)]="selectedTheme">
14
+ * <sng-menu-radio-item value="light">Light</sng-menu-radio-item>
15
+ * <sng-menu-radio-item value="dark">Dark</sng-menu-radio-item>
16
+ * <sng-menu-radio-item value="system">System</sng-menu-radio-item>
17
+ * </sng-menu-radio-group>
18
+ * </sng-menu>
19
+ * ```
20
+ */
21
+ @Component({
22
+ selector: 'sng-menu-radio-group',
23
+ standalone: true,
24
+ changeDetection: ChangeDetectionStrategy.OnPush,
25
+ host: {
26
+ 'role': 'group',
27
+ },
28
+ template: `<ng-content />`,
29
+ })
30
+ export class SngMenuRadioGroup {
31
+ /** The currently selected value. Supports two-way binding. */
32
+ value = model<string>('');
33
+
34
+ /** @internal Called by child radio items to select a value. */
35
+ _selectValue(newValue: string) {
36
+ this.value.set(newValue);
37
+ }
38
+ }
@@ -0,0 +1,94 @@
1
+ import {
2
+ Component,
3
+ ChangeDetectionStrategy,
4
+ ViewEncapsulation,
5
+ inject,
6
+ input,
7
+ computed,
8
+ booleanAttribute,
9
+ } from '@angular/core';
10
+ import { cn } from './cn';
11
+ import { SngMenuRadioGroup } from './sng-menu-radio-group';
12
+ import { SNG_MENU_PANEL, MENU_ITEM_BASE_CLASSES } from './sng-menu-tokens';
13
+
14
+ /**
15
+ * A radio menu item within a radio group.
16
+ *
17
+ * @example
18
+ * ```html
19
+ * <sng-menu-radio-group [(value)]="theme">
20
+ * <sng-menu-radio-item value="light">Light</sng-menu-radio-item>
21
+ * <sng-menu-radio-item value="dark">Dark</sng-menu-radio-item>
22
+ * </sng-menu-radio-group>
23
+ * ```
24
+ */
25
+ @Component({
26
+ selector: 'sng-menu-radio-item',
27
+ standalone: true,
28
+ changeDetection: ChangeDetectionStrategy.OnPush,
29
+ encapsulation: ViewEncapsulation.None,
30
+ host: {
31
+ '[class]': 'hostClasses()',
32
+ '[attr.data-state]': 'isChecked() ? "checked" : "unchecked"',
33
+ '[attr.data-disabled]': 'resolvedDisabled() ? "" : null',
34
+ '[attr.aria-checked]': 'isChecked()',
35
+ '[attr.tabindex]': 'resolvedDisabled() ? -1 : 0',
36
+ 'role': 'menuitemradio',
37
+ '(click)': 'select()',
38
+ },
39
+ template: `
40
+ <span class="flex h-4 w-4 items-center justify-center mr-2">
41
+ @if (isChecked()) {
42
+ <svg class="h-2 w-2 fill-current" viewBox="0 0 8 8">
43
+ <circle cx="4" cy="4" r="4" />
44
+ </svg>
45
+ }
46
+ </span>
47
+ <ng-content />
48
+ `,
49
+ })
50
+ export class SngMenuRadioItem {
51
+ private radioGroup = inject(SngMenuRadioGroup, { optional: true });
52
+ private panel = inject(SNG_MENU_PANEL, { optional: true });
53
+
54
+ /** Custom CSS classes. */
55
+ class = input<string>('');
56
+
57
+ /** The value associated with this radio item. */
58
+ value = input.required<string>();
59
+
60
+ /** Whether the radio item is disabled. */
61
+ disabled = input(false, { transform: booleanAttribute });
62
+
63
+ /** Legacy disabled input name. */
64
+ isDisabled = input<unknown>(undefined);
65
+
66
+ /** Whether to close the menu when this item is selected. Uses parent menu's setting when not specified. */
67
+ isCloseOnSelect = input<boolean | undefined>(undefined);
68
+
69
+ resolvedDisabled = computed(() => {
70
+ const legacyValue = this.isDisabled();
71
+ return legacyValue === undefined ? this.disabled() : booleanAttribute(legacyValue);
72
+ });
73
+
74
+ /** Whether this item is currently checked. */
75
+ isChecked = computed(() => this.radioGroup?.value() === this.value());
76
+
77
+ hostClasses = computed(() =>
78
+ cn(
79
+ ...MENU_ITEM_BASE_CLASSES,
80
+ this.class()
81
+ )
82
+ );
83
+
84
+ /** Select this radio item. */
85
+ select() {
86
+ if (!this.resolvedDisabled() && this.radioGroup) {
87
+ this.radioGroup._selectValue(this.value());
88
+ const shouldClose = this.isCloseOnSelect() ?? this.panel?.closeOnSelect() ?? true;
89
+ if (shouldClose) {
90
+ this.panel?.close();
91
+ }
92
+ }
93
+ }
94
+ }
@@ -0,0 +1,27 @@
1
+ import { Component, ChangeDetectionStrategy } from '@angular/core';
2
+
3
+ /**
4
+ * Visual divider line between groups of menu items.
5
+ *
6
+ * @example
7
+ * ```html
8
+ * <sng-menu>
9
+ * <sng-menu-item>Undo</sng-menu-item>
10
+ * <sng-menu-item>Redo</sng-menu-item>
11
+ * <sng-menu-separator />
12
+ * <sng-menu-item>Cut</sng-menu-item>
13
+ * <sng-menu-item>Copy</sng-menu-item>
14
+ * </sng-menu>
15
+ * ```
16
+ */
17
+ @Component({
18
+ selector: 'sng-menu-separator',
19
+ standalone: true,
20
+ changeDetection: ChangeDetectionStrategy.OnPush,
21
+ host: {
22
+ 'class': 'block bg-border -mx-1 my-1 h-px',
23
+ 'role': 'separator',
24
+ },
25
+ template: '',
26
+ })
27
+ export class SngMenuSeparator {}
@@ -0,0 +1,25 @@
1
+ import { Component, ChangeDetectionStrategy } from '@angular/core';
2
+
3
+ /**
4
+ * Displays a shortcut hint aligned to the right side of a menu item.
5
+ *
6
+ * @example
7
+ * ```html
8
+ * <sng-menu>
9
+ * <sng-menu-item>
10
+ * Save
11
+ * <sng-menu-shortcut>Ctrl+S</sng-menu-shortcut>
12
+ * </sng-menu-item>
13
+ * </sng-menu>
14
+ * ```
15
+ */
16
+ @Component({
17
+ selector: 'sng-menu-shortcut',
18
+ standalone: true,
19
+ changeDetection: ChangeDetectionStrategy.OnPush,
20
+ host: {
21
+ 'class': 'ml-auto text-xs tracking-widest text-muted-foreground',
22
+ },
23
+ template: `<ng-content />`,
24
+ })
25
+ export class SngMenuShortcut {}
@@ -0,0 +1,267 @@
1
+ import {
2
+ Component,
3
+ ChangeDetectionStrategy,
4
+ ViewEncapsulation,
5
+ input,
6
+ computed,
7
+ inject,
8
+ Injector,
9
+ effect,
10
+ contentChildren,
11
+ forwardRef,
12
+ TemplateRef,
13
+ viewChild,
14
+ ViewContainerRef,
15
+ OnDestroy,
16
+ afterNextRender,
17
+ } from '@angular/core';
18
+ import { Overlay, type OverlayRef, type ConnectedPosition } from '@angular/cdk/overlay';
19
+ import { TemplatePortal } from '@angular/cdk/portal';
20
+ import { cn } from './cn';
21
+ import { SNG_MENU_PANEL, type MenuPanel, animateOverlayClose, focusMenuContent } from './sng-menu-tokens';
22
+ import { SngMenu } from './sng-menu';
23
+ import { SngMenuSub, type MenuContentCoordinator } from './sng-menu-sub';
24
+
25
+ const SUB_POSITIONS: ConnectedPosition[] = [
26
+ { originX: 'end', originY: 'top', overlayX: 'start', overlayY: 'top', offsetX: 4 },
27
+ { originX: 'start', originY: 'top', overlayX: 'end', overlayY: 'top', offsetX: -4 },
28
+ ];
29
+
30
+ /**
31
+ * Container for submenu items. Uses CDK Overlay for viewport-aware positioning.
32
+ *
33
+ * @example
34
+ * ```html
35
+ * <sng-menu-sub>
36
+ * <sng-menu-sub-trigger>More</sng-menu-sub-trigger>
37
+ * <sng-menu-sub-content>
38
+ * <sng-menu-item>Nested Item 1</sng-menu-item>
39
+ * </sng-menu-sub-content>
40
+ * </sng-menu-sub>
41
+ * ```
42
+ */
43
+ @Component({
44
+ selector: 'sng-menu-sub-content',
45
+ standalone: true,
46
+ changeDetection: ChangeDetectionStrategy.OnPush,
47
+ encapsulation: ViewEncapsulation.None,
48
+ providers: [
49
+ { provide: SNG_MENU_PANEL, useExisting: SngMenuSubContent },
50
+ ],
51
+ styles: [`
52
+ .sng-menu-sub-panel[data-state=open] { animation: sng-menu-sub-enter 150ms ease both; }
53
+ .sng-menu-sub-panel[data-state=closed] { animation: sng-menu-sub-exit 150ms ease both; }
54
+ @keyframes sng-menu-sub-enter { from { opacity: 0; transform: scale(0.95) translateX(-0.5rem); } }
55
+ @keyframes sng-menu-sub-exit { to { opacity: 0; transform: scale(0.95); } }
56
+ `],
57
+ template: `
58
+ <ng-template #subTemplate>
59
+ <div
60
+ [class]="contentClasses()"
61
+ [attr.data-state]="parentSub.isOpen() ? 'open' : 'closed'"
62
+ role="menu"
63
+ tabindex="-1">
64
+ <ng-content />
65
+ </div>
66
+ </ng-template>
67
+ `,
68
+ })
69
+ export class SngMenuSubContent implements MenuPanel, MenuContentCoordinator, OnDestroy {
70
+ /** @internal */
71
+ parentSub = inject(SngMenuSub);
72
+ private rootPanel = inject(SNG_MENU_PANEL, { skipSelf: true });
73
+ private rootMenu = inject(SngMenu);
74
+ private overlay = inject(Overlay);
75
+ private injector = inject(Injector);
76
+ private viewContainerRef = inject(ViewContainerRef);
77
+
78
+ private subTemplate = viewChild<TemplateRef<unknown>>('subTemplate');
79
+ private overlayRef: OverlayRef | null = null;
80
+ private _closing = false;
81
+ private _hoverEnterHandler: ((e: Event) => void) | null = null;
82
+ private _hoverLeaveHandler: ((e: Event) => void) | null = null;
83
+
84
+ /** @internal Track nested submenus for coordination. */
85
+ private nestedSubs = contentChildren(forwardRef(() => SngMenuSub), { descendants: true });
86
+
87
+ /** @internal Currently open nested submenu. */
88
+ private openSubmenu: SngMenuSub | null = null;
89
+
90
+ /** Custom CSS classes. */
91
+ class = input<string>('');
92
+
93
+ /** Inherits closeOnSelect from the root menu. */
94
+ get closeOnSelect() { return this.rootPanel.closeOnSelect; }
95
+
96
+ contentClasses = computed(() =>
97
+ cn(
98
+ 'z-[9999] min-w-[8rem] rounded-md border border-border bg-popover py-1 text-popover-foreground shadow-lg',
99
+ 'flex flex-col sng-menu-sub-panel',
100
+ this.class()
101
+ )
102
+ );
103
+
104
+ constructor() {
105
+ // Register with root menu for synchronous cleanup on close
106
+ this.rootMenu._subContentOverlays.add(this);
107
+
108
+ // Register cascade-dispose callback so parent can synchronously tear us down
109
+ this.parentSub._contentDispose = () => this._cascadeDispose();
110
+
111
+ // Register as coordinator for nested submenus
112
+ effect(() => {
113
+ const subs = this.nestedSubs();
114
+ subs.forEach(sub => { sub._parentCoordinator = this; });
115
+ });
116
+
117
+ // React to parent sub open/close
118
+ effect(() => {
119
+ const isOpen = this.parentSub.isOpen();
120
+ if (isOpen) {
121
+ this.openOverlay();
122
+ } else {
123
+ this.closeOverlay();
124
+ }
125
+ });
126
+ }
127
+
128
+ /** Close the entire menu tree. */
129
+ close() {
130
+ this.rootPanel.close();
131
+ }
132
+
133
+ /** @internal */
134
+ _requestSubOpen(sub: SngMenuSub): void {
135
+ if (this.openSubmenu && this.openSubmenu !== sub) {
136
+ // Cascade-dispose any nested overlays synchronously before closing the sibling
137
+ this.openSubmenu._contentDispose?.();
138
+ this.openSubmenu.closeImmediate();
139
+ }
140
+ this.openSubmenu = sub;
141
+ }
142
+
143
+ /** @internal */
144
+ _notifySubClosed(sub: SngMenuSub): void {
145
+ if (this.openSubmenu === sub) {
146
+ this.openSubmenu = null;
147
+ }
148
+ }
149
+
150
+ /** @internal */
151
+ _shouldOpenImmediately(sub: SngMenuSub): boolean {
152
+ return this.openSubmenu !== null && this.openSubmenu !== sub;
153
+ }
154
+
155
+ /** @internal Reset all state when root menu closes. */
156
+ _resetOnMenuClose() {
157
+ this.parentSub._reset();
158
+ if (this.openSubmenu) {
159
+ this.openSubmenu._contentDispose?.();
160
+ this.openSubmenu._reset();
161
+ this.openSubmenu = null;
162
+ }
163
+ this._closing = false;
164
+ this._disposeOverlay();
165
+ }
166
+
167
+ /** @internal Cancel pending close on this submenu and all ancestor submenus. */
168
+ _keepAncestorChainOpen() {
169
+ this.parentSub.scheduleOpen();
170
+ const coord = this.parentSub._parentCoordinator;
171
+ if (coord instanceof SngMenuSubContent) {
172
+ coord._keepAncestorChainOpen();
173
+ }
174
+ }
175
+
176
+ /** @internal Synchronously tear down this overlay and all nested child overlays. */
177
+ _cascadeDispose() {
178
+ // Cascade down: dispose any open nested submenu's overlay first
179
+ if (this.openSubmenu) {
180
+ this.openSubmenu._contentDispose?.();
181
+ this.openSubmenu._reset();
182
+ this.openSubmenu = null;
183
+ }
184
+ this._closing = false;
185
+ this._disposeOverlay();
186
+ }
187
+
188
+ ngOnDestroy() {
189
+ this.parentSub._contentDispose = null;
190
+ this.rootMenu._subContentOverlays.delete(this);
191
+ // Force-dispose: bypass _closing guard since the component is being destroyed
192
+ this._closing = false;
193
+ this._disposeOverlay();
194
+ }
195
+
196
+ private openOverlay() {
197
+ const template = this.subTemplate();
198
+ const triggerEl = this.parentSub._subTrigger()?._elementRef.nativeElement;
199
+ if (!template || !triggerEl) return;
200
+
201
+ this._disposeOverlay();
202
+
203
+ const positionStrategy = this.overlay.position()
204
+ .flexibleConnectedTo(triggerEl)
205
+ .withPositions(SUB_POSITIONS)
206
+ .withPush(true)
207
+ .withViewportMargin(8);
208
+
209
+ this.overlayRef = this.overlay.create({
210
+ positionStrategy,
211
+ scrollStrategy: this.overlay.scrollStrategies.reposition(),
212
+ });
213
+
214
+ const portal = new TemplatePortal(template, this.viewContainerRef);
215
+ this.overlayRef.attach(portal);
216
+
217
+ // Keep submenu open when mouse is over the overlay panel
218
+ const overlayEl = this.overlayRef.overlayElement;
219
+ this._hoverEnterHandler = () => this._keepAncestorChainOpen();
220
+ this._hoverLeaveHandler = (e: Event) => {
221
+ const related = (e as MouseEvent).relatedTarget as HTMLElement | null;
222
+ // If mouse went back to the parent sng-menu-sub, don't close
223
+ if (related?.closest('sng-menu-sub') === this.parentSub._hostEl) return;
224
+ // If mouse went to a deeper nested submenu panel, don't close
225
+ if (related?.closest('.sng-menu-sub-panel')) return;
226
+ this.parentSub.scheduleClose();
227
+ };
228
+ overlayEl.addEventListener('mouseenter', this._hoverEnterHandler);
229
+ overlayEl.addEventListener('mouseleave', this._hoverLeaveHandler);
230
+
231
+ afterNextRender(() => {
232
+ const panel = this.overlayRef?.overlayElement.querySelector('[role="menu"]') as HTMLElement | null;
233
+ if (panel) {
234
+ focusMenuContent(panel);
235
+ }
236
+ }, { injector: this.injector });
237
+ }
238
+
239
+ private closeOverlay() {
240
+ if (!this.overlayRef || this._closing) return;
241
+ this._closing = true;
242
+ // Synchronously cascade-close all nested overlays before animating this one
243
+ if (this.openSubmenu) {
244
+ this.openSubmenu._contentDispose?.();
245
+ this.openSubmenu._reset();
246
+ this.openSubmenu = null;
247
+ }
248
+ animateOverlayClose(this.overlayRef, () => this._disposeOverlay());
249
+ }
250
+
251
+ /** @internal */ _disposeOverlay() {
252
+ this._closing = false;
253
+ if (this.overlayRef) {
254
+ const overlayEl = this.overlayRef.overlayElement;
255
+ if (this._hoverEnterHandler) {
256
+ overlayEl.removeEventListener('mouseenter', this._hoverEnterHandler);
257
+ this._hoverEnterHandler = null;
258
+ }
259
+ if (this._hoverLeaveHandler) {
260
+ overlayEl.removeEventListener('mouseleave', this._hoverLeaveHandler);
261
+ this._hoverLeaveHandler = null;
262
+ }
263
+ this.overlayRef.dispose();
264
+ this.overlayRef = null;
265
+ }
266
+ }
267
+ }
@@ -0,0 +1,68 @@
1
+ import {
2
+ Component,
3
+ ChangeDetectionStrategy,
4
+ ViewEncapsulation,
5
+ ElementRef,
6
+ input,
7
+ inject,
8
+ computed,
9
+ } from '@angular/core';
10
+ import { cn } from './cn';
11
+
12
+ /**
13
+ * Trigger element for a submenu. Displays a chevron icon indicating
14
+ * a nested menu is available on hover.
15
+ *
16
+ * @example
17
+ * ```html
18
+ * <sng-menu-sub>
19
+ * <sng-menu-sub-trigger>Share</sng-menu-sub-trigger>
20
+ * <sng-menu-sub-content>
21
+ * <sng-menu-item>Email</sng-menu-item>
22
+ * </sng-menu-sub-content>
23
+ * </sng-menu-sub>
24
+ * ```
25
+ */
26
+ @Component({
27
+ selector: 'sng-menu-sub-trigger',
28
+ standalone: true,
29
+ changeDetection: ChangeDetectionStrategy.OnPush,
30
+ encapsulation: ViewEncapsulation.None,
31
+ host: {
32
+ '[class]': 'hostClasses()',
33
+ 'role': 'menuitem',
34
+ 'aria-haspopup': 'menu',
35
+ 'tabindex': '0',
36
+ },
37
+ template: `
38
+ <ng-content />
39
+ <svg
40
+ class="ml-auto h-4 w-4"
41
+ xmlns="http://www.w3.org/2000/svg"
42
+ viewBox="0 0 24 24"
43
+ fill="none"
44
+ stroke="currentColor"
45
+ stroke-width="2"
46
+ stroke-linecap="round"
47
+ stroke-linejoin="round"
48
+ >
49
+ <path d="m9 18 6-6-6-6"/>
50
+ </svg>
51
+ `,
52
+ })
53
+ export class SngMenuSubTrigger {
54
+ /** @internal Element ref for overlay positioning. */
55
+ _elementRef = inject(ElementRef);
56
+
57
+ /** Custom CSS classes. */
58
+ class = input<string>('');
59
+
60
+ hostClasses = computed(() =>
61
+ cn(
62
+ 'flex cursor-default select-none items-center justify-between gap-4 whitespace-nowrap rounded-sm px-2 py-1.5 text-sm outline-none',
63
+ 'hover:bg-accent hover:text-accent-foreground',
64
+ 'focus-visible:bg-accent focus-visible:text-accent-foreground',
65
+ this.class()
66
+ )
67
+ );
68
+ }
@@ -0,0 +1,124 @@
1
+ import {
2
+ Component,
3
+ ChangeDetectionStrategy,
4
+ OnDestroy,
5
+ ElementRef,
6
+ inject,
7
+ signal,
8
+ contentChild,
9
+ } from '@angular/core';
10
+ import { Subscription, timer } from 'rxjs';
11
+ import { SngMenuSubTrigger } from './sng-menu-sub-trigger';
12
+
13
+ /** Interface for parent content to coordinate sibling submenus. */
14
+ export interface MenuContentCoordinator {
15
+ _requestSubOpen(sub: SngMenuSub): void;
16
+ _notifySubClosed(sub: SngMenuSub): void;
17
+ _shouldOpenImmediately(sub: SngMenuSub): boolean;
18
+ }
19
+
20
+ /**
21
+ * Container for a nested submenu within a menu.
22
+ * Opens on hover and contains a trigger and content.
23
+ *
24
+ * @example
25
+ * ```html
26
+ * <sng-menu-sub>
27
+ * <sng-menu-sub-trigger>More Options</sng-menu-sub-trigger>
28
+ * <sng-menu-sub-content>
29
+ * <sng-menu-item>Option A</sng-menu-item>
30
+ * </sng-menu-sub-content>
31
+ * </sng-menu-sub>
32
+ * ```
33
+ */
34
+ @Component({
35
+ selector: 'sng-menu-sub',
36
+ standalone: true,
37
+ changeDetection: ChangeDetectionStrategy.OnPush,
38
+ host: {
39
+ 'class': 'block relative',
40
+ '[attr.data-state]': 'isOpen() ? "open" : "closed"',
41
+ '(mouseenter)': 'scheduleOpen()',
42
+ '(mouseleave)': 'onMouseLeave($event)',
43
+ },
44
+ template: `<ng-content />`,
45
+ })
46
+ export class SngMenuSub implements OnDestroy {
47
+ private hoverTimerSubscription: Subscription | null = null;
48
+
49
+ /** @internal Host element reference. */
50
+ _hostEl = inject(ElementRef).nativeElement;
51
+
52
+ /** @internal The sub-trigger child for positioning. */
53
+ _subTrigger = contentChild(SngMenuSubTrigger);
54
+
55
+ /** @internal Set by parent content for sibling coordination. */
56
+ _parentCoordinator: MenuContentCoordinator | null = null;
57
+
58
+ /** @internal Dispose callback set by content for cascade close. */
59
+ _contentDispose: (() => void) | null = null;
60
+
61
+ isOpen = signal(false);
62
+
63
+ open() {
64
+ if (this.isOpen()) return;
65
+ this.clearTimeout();
66
+ this.isOpen.set(true);
67
+ this._parentCoordinator?._requestSubOpen(this);
68
+ }
69
+
70
+ close() {
71
+ if (!this.isOpen()) return;
72
+ this.isOpen.set(false);
73
+ this._parentCoordinator?._notifySubClosed(this);
74
+ }
75
+
76
+ /** Immediately close without animation — used when switching between submenus. */
77
+ closeImmediate() {
78
+ this.clearTimeout();
79
+ this.isOpen.set(false);
80
+ }
81
+
82
+ scheduleOpen() {
83
+ this.clearTimeout();
84
+ if (this._parentCoordinator?._shouldOpenImmediately(this)) {
85
+ this.open();
86
+ } else {
87
+ this.hoverTimerSubscription = timer(100).subscribe(() => {
88
+ this.hoverTimerSubscription = null;
89
+ this.open();
90
+ });
91
+ }
92
+ }
93
+
94
+ /** @internal */
95
+ onMouseLeave(event: MouseEvent) {
96
+ const related = event.relatedTarget as HTMLElement | null;
97
+ // If mouse moved into the submenu overlay panel, don't close
98
+ if (related?.closest('.sng-menu-sub-panel')) return;
99
+ this.scheduleClose();
100
+ }
101
+
102
+ scheduleClose() {
103
+ this.clearTimeout();
104
+ this.hoverTimerSubscription = timer(100).subscribe(() => {
105
+ this.hoverTimerSubscription = null;
106
+ this.close();
107
+ });
108
+ }
109
+
110
+ /** @internal Reset all state when root menu closes. */
111
+ _reset() {
112
+ this.clearTimeout();
113
+ this.isOpen.set(false);
114
+ }
115
+
116
+ private clearTimeout() {
117
+ this.hoverTimerSubscription?.unsubscribe();
118
+ this.hoverTimerSubscription = null;
119
+ }
120
+
121
+ ngOnDestroy() {
122
+ this.clearTimeout();
123
+ }
124
+ }