@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,97 @@
1
+ import {
2
+ Component,
3
+ ChangeDetectionStrategy,
4
+ ViewEncapsulation,
5
+ input,
6
+ computed,
7
+ inject,
8
+ booleanAttribute,
9
+ } from '@angular/core';
10
+ import { SNG_LAYOUT_SIDEBAR_CONTEXT } from './sng-layout-sidebar-provider';
11
+ import { cn } from './cn';
12
+
13
+ /**
14
+ * Button that toggles the sidebar. Automatically calls `toggle()` on desktop or
15
+ * `toggleMobile()` on mobile — no conditional logic needed in your template.
16
+ *
17
+ * @example
18
+ * ```html
19
+ * <!-- Default icon -->
20
+ * <sng-layout-sidebar-trigger />
21
+ *
22
+ * <!-- Custom content -->
23
+ * <sng-layout-sidebar-trigger [customContent]="true">
24
+ * <lucide-icon name="menu" />
25
+ * </sng-layout-sidebar-trigger>
26
+ * ```
27
+ */
28
+ @Component({
29
+ selector: 'sng-layout-sidebar-trigger',
30
+ standalone: true,
31
+ changeDetection: ChangeDetectionStrategy.OnPush,
32
+ encapsulation: ViewEncapsulation.None,
33
+ host: {
34
+ 'class': 'contents',
35
+ },
36
+ template: `
37
+ <button
38
+ type="button"
39
+ [class]="buttonClasses()"
40
+ [disabled]="disabled()"
41
+ (click)="onClick()"
42
+ >
43
+ @if (customContent()) {
44
+ <ng-content />
45
+ } @else {
46
+ <svg
47
+ xmlns="http://www.w3.org/2000/svg"
48
+ width="24"
49
+ height="24"
50
+ viewBox="0 0 24 24"
51
+ fill="none"
52
+ stroke="currentColor"
53
+ stroke-width="2"
54
+ stroke-linecap="round"
55
+ stroke-linejoin="round"
56
+ class="size-4"
57
+ >
58
+ <rect width="18" height="18" x="3" y="3" rx="2" />
59
+ <path d="M9 3v18" />
60
+ </svg>
61
+ <span class="sr-only">{{ toggleLabel() }}</span>
62
+ }
63
+ </button>
64
+ `,
65
+ })
66
+ export class SngLayoutSidebarTrigger {
67
+ /** Custom CSS classes. */
68
+ class = input<string>('');
69
+
70
+ /** Whether the button is disabled. */
71
+ disabled = input(false, { transform: booleanAttribute });
72
+
73
+ /**
74
+ * When true, renders projected content instead of the default icon.
75
+ */
76
+ customContent = input(false, { transform: booleanAttribute });
77
+
78
+ /** Accessible text for the default toggle control. */
79
+ toggleLabel = input<string>('Toggle Sidebar');
80
+
81
+ private context = inject(SNG_LAYOUT_SIDEBAR_CONTEXT);
82
+
83
+ buttonClasses = computed(() =>
84
+ cn(
85
+ 'inline-flex h-7 w-7 items-center justify-center rounded-md text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50',
86
+ this.class()
87
+ )
88
+ );
89
+
90
+ onClick(): void {
91
+ if (this.context.isMobile()) {
92
+ this.context.toggleMobile();
93
+ } else {
94
+ this.context.toggle();
95
+ }
96
+ }
97
+ }
@@ -0,0 +1,254 @@
1
+ import {
2
+ Component,
3
+ ChangeDetectionStrategy,
4
+ ChangeDetectorRef,
5
+ ViewEncapsulation,
6
+ DestroyRef,
7
+ input,
8
+ computed,
9
+ inject,
10
+ effect,
11
+ afterNextRender,
12
+ DOCUMENT,
13
+ } from '@angular/core';
14
+ import { SNG_LAYOUT_SIDEBAR_CONTEXT } from './sng-layout-sidebar-provider';
15
+ import { cn } from './cn';
16
+
17
+ export type LayoutSidebarSide = 'left' | 'right';
18
+ export type LayoutSidebarCollapsible = 'offcanvas' | 'icon' | 'none';
19
+ export type LayoutSidebarLayout = 'sidebar' | 'floating' | 'inset';
20
+
21
+ /**
22
+ * Main sidebar component that renders as a fixed/sticky panel on desktop (≥768px) or a
23
+ * drawer overlay on mobile (<768px). The dual rendering is automatic — the same template
24
+ * switches between `containerClasses()` (desktop) and `mobileDrawerClasses()` (mobile)
25
+ * based on `context.isMobile()`.
26
+ *
27
+ * On mobile: renders a fixed drawer with slide animation, backdrop overlay, body scroll
28
+ * lock. On desktop: renders a fixed/sticky panel with configurable
29
+ * `layout` and `collapsible` modes.
30
+ *
31
+ * To customize the mobile breakpoint, change the `matchMedia` query in the provider and
32
+ * update the `md:` Tailwind prefixes in this component (hostClasses, containerClasses).
33
+ *
34
+ * @example
35
+ * ```html
36
+ * <sng-layout-sidebar-provider>
37
+ * <sng-layout-sidebar side="left" collapsible="icon">
38
+ * <sng-layout-sidebar-header>Logo</sng-layout-sidebar-header>
39
+ * <sng-layout-sidebar-content>
40
+ * <sng-layout-sidebar-menu>...</sng-layout-sidebar-menu>
41
+ * </sng-layout-sidebar-content>
42
+ * <sng-layout-sidebar-footer>User</sng-layout-sidebar-footer>
43
+ * </sng-layout-sidebar>
44
+ * <sng-layout-sidebar-inset>Main content</sng-layout-sidebar-inset>
45
+ * </sng-layout-sidebar-provider>
46
+ * ```
47
+ */
48
+ @Component({
49
+ selector: 'sng-layout-sidebar',
50
+ standalone: true,
51
+ changeDetection: ChangeDetectionStrategy.OnPush,
52
+ encapsulation: ViewEncapsulation.None,
53
+ styles: `
54
+ /* Remap accent colors to sidebar-accent for menus inside sidebar */
55
+ sng-layout-sidebar {
56
+ --accent: var(--sidebar-accent);
57
+ --accent-foreground: var(--sidebar-accent-foreground);
58
+ /* Darker variant for selected/active state - light mode: darken, dark mode: lighten */
59
+ --sidebar-active: color-mix(in oklch, var(--sidebar-accent), black 8%);
60
+ }
61
+ /* Dark mode: lighten instead of darken */
62
+ .dark sng-layout-sidebar {
63
+ --sidebar-active: color-mix(in oklch, var(--sidebar-accent), white 15%);
64
+ }
65
+
66
+ /* Mobile overlay animations */
67
+ .sng-sidebar-overlay[data-state=open] { animation: sng-sidebar-overlay-in 200ms ease both; }
68
+ .sng-sidebar-overlay[data-state=closed] { animation: sng-sidebar-overlay-out 200ms ease both; }
69
+ @keyframes sng-sidebar-overlay-in { from { opacity: 0; } }
70
+ @keyframes sng-sidebar-overlay-out { to { opacity: 0; } }
71
+
72
+ /* Mobile drawer slide animations */
73
+ .sng-sidebar-drawer[data-state=open][data-side=left] { animation: sng-sidebar-slide-in-left 200ms ease both; }
74
+ .sng-sidebar-drawer[data-state=closed][data-side=left] { animation: sng-sidebar-slide-out-left 200ms ease both; }
75
+ .sng-sidebar-drawer[data-state=open][data-side=right] { animation: sng-sidebar-slide-in-right 200ms ease both; }
76
+ .sng-sidebar-drawer[data-state=closed][data-side=right] { animation: sng-sidebar-slide-out-right 200ms ease both; }
77
+ @keyframes sng-sidebar-slide-in-left { from { transform: translateX(-100%); } }
78
+ @keyframes sng-sidebar-slide-out-left { to { transform: translateX(-100%); } }
79
+ @keyframes sng-sidebar-slide-in-right { from { transform: translateX(100%); } }
80
+ @keyframes sng-sidebar-slide-out-right { to { transform: translateX(100%); } }
81
+ `,
82
+ template: `
83
+ <!-- Mobile overlay backdrop (only rendered on mobile after first open) -->
84
+ @if (context.isMobile() && hasBeenOpened()) {
85
+ <button type="button"
86
+ class="sng-sidebar-overlay fixed inset-0 z-50 appearance-none border-0 bg-black/50 p-0"
87
+ [class.pointer-events-none]="!context.openMobile()"
88
+ [attr.data-state]="mobileState()"
89
+ [attr.tabindex]="context.openMobile() ? 0 : -1"
90
+ [attr.aria-label]="'Close sidebar'"
91
+ (click)="onBackdropClick()"></button>
92
+ }
93
+
94
+ <!-- Desktop gap div (reserves space in document flow) -->
95
+ @if (!context.isMobile()) {
96
+ <div [class]="gapClasses()"></div>
97
+ }
98
+
99
+ <!-- Shared container: switches between desktop fixed and mobile drawer -->
100
+ <div [class]="context.isMobile() ? mobileDrawerClasses() : containerClasses()"
101
+ [attr.data-state]="context.isMobile() ? mobileState() : context.state()"
102
+ [attr.data-collapsible]="context.isMobile() ? null : collapsibleAttr()"
103
+ [attr.data-side]="context.isMobile() ? side() : null"
104
+ [class.pointer-events-none]="context.isMobile() && !context.openMobile()"
105
+ [attr.aria-hidden]="context.isMobile() && !context.openMobile() ? 'true' : null"
106
+ [attr.inert]="context.isMobile() && !context.openMobile() ? '' : null">
107
+ <div [class]="innerClasses()"
108
+ [attr.data-state]="context.isMobile() ? null : context.state()"
109
+ [attr.data-collapsible]="context.isMobile() ? null : collapsibleAttr()">
110
+ <ng-content />
111
+ </div>
112
+ </div>
113
+ `,
114
+ host: {
115
+ // Static fallback prevents FOUC with zoneless CD (dynamic [class] may not flush immediately)
116
+ 'class': 'group peer text-sidebar-foreground hidden md:block',
117
+ '[class]': 'hostClasses()',
118
+ '[style.display]': 'hostDisplay()',
119
+ '[attr.data-state]': 'context.isMobile() ? mobileState() : context.state()',
120
+ '[attr.data-collapsible]': 'collapsibleAttr()',
121
+ '[attr.data-side]': 'side()',
122
+ '[attr.data-layout]': 'layout()',
123
+ },
124
+ })
125
+ export class SngLayoutSidebar {
126
+ /** Which side of the viewport the sidebar appears on. */
127
+ side = input<LayoutSidebarSide>('left');
128
+
129
+ /** Collapse behavior: 'offcanvas' slides out, 'icon' shows icons only, 'none' disables collapse. */
130
+ collapsible = input<LayoutSidebarCollapsible>('offcanvas');
131
+
132
+ /**
133
+ * Layout mode controlling positioning and visual style.
134
+ * - 'sidebar': Fixed positioning with border (default)
135
+ * - 'floating': Fixed with padding, rounded corners, border, and shadow
136
+ * - 'inset': Sticky positioning within content flow
137
+ */
138
+ layout = input<LayoutSidebarLayout>('sidebar');
139
+
140
+ /** Custom CSS classes. */
141
+ class = input<string>('');
142
+
143
+ protected context = inject(SNG_LAYOUT_SIDEBAR_CONTEXT);
144
+ private doc = inject(DOCUMENT);
145
+ private cdr = inject(ChangeDetectorRef);
146
+ private destroyRef = inject(DestroyRef);
147
+
148
+ // data-collapsible only shows value when collapsed
149
+ collapsibleAttr = computed(() =>
150
+ this.context.state() === 'collapsed' ? this.collapsible() : ''
151
+ );
152
+
153
+ // Mobile drawer state
154
+ mobileState = computed(() =>
155
+ this.context.openMobile() ? 'open' : 'closed'
156
+ );
157
+
158
+ // Track if mobile drawer has ever been opened (avoid rendering closed drawer on page load)
159
+ private _hasBeenOpened = false;
160
+ hasBeenOpened = computed(() => {
161
+ if (this.context.openMobile()) this._hasBeenOpened = true;
162
+ return this._hasBeenOpened;
163
+ });
164
+
165
+ // Host element - outer wrapper
166
+ hostClasses = computed(() => cn(
167
+ 'group peer text-sidebar-foreground',
168
+ this.class()
169
+ ));
170
+
171
+ hostDisplay = computed(() => (this.context.isMobile() ? 'block' : null));
172
+
173
+ // Gap div - reserves space in document flow (desktop only)
174
+ gapClasses = computed(() => {
175
+ const s = this.side();
176
+ const v = this.layout();
177
+ const noTransition = this.context.suppressTransition();
178
+ return cn(
179
+ 'relative bg-transparent',
180
+ !noTransition && 'transition-[width] duration-200 ease-linear',
181
+ 'w-(--sidebar-width)',
182
+ 'group-data-[collapsible=offcanvas]:w-0',
183
+ v === 'floating'
184
+ ? 'group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+1rem)]'
185
+ : 'group-data-[collapsible=icon]:w-(--sidebar-width-icon)',
186
+ s === 'right' && 'order-1'
187
+ );
188
+ });
189
+
190
+ // Container - positions the sidebar based on layout mode (desktop only)
191
+ containerClasses = computed(() => {
192
+ const s = this.side();
193
+ const v = this.layout();
194
+ const isInset = v === 'inset';
195
+ const noTransition = this.context.suppressTransition();
196
+
197
+ return cn(
198
+ 'z-10 hidden w-(--sidebar-width) md:flex',
199
+ !noTransition && 'transition-[inset-inline-start,inset-inline-end,width] duration-200 ease-linear',
200
+ isInset ? 'sticky top-0 h-svh' : 'fixed inset-y-0 h-full',
201
+ !isInset && (s === 'left' ? 'start-0' : 'end-0'),
202
+ v === 'floating' && 'p-2',
203
+ isInset
204
+ ? 'group-data-[collapsible=offcanvas]:w-0'
205
+ : s === 'left'
206
+ ? 'group-data-[collapsible=offcanvas]:start-[calc(var(--sidebar-width)*-1)]'
207
+ : 'group-data-[collapsible=offcanvas]:end-[calc(var(--sidebar-width)*-1)]',
208
+ v === 'floating'
209
+ ? 'group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+1rem+2px)]'
210
+ : 'group-data-[collapsible=icon]:w-(--sidebar-width-icon)',
211
+ v === 'sidebar' && (s === 'left' ? 'border-e' : 'border-s')
212
+ );
213
+ });
214
+
215
+ // Mobile drawer classes
216
+ mobileDrawerClasses = computed(() => {
217
+ const s = this.side();
218
+ return cn(
219
+ 'sng-sidebar-drawer fixed inset-y-0 z-50 flex w-(--sidebar-width) shadow-lg',
220
+ s === 'left' ? 'left-0 border-e' : 'right-0 border-s',
221
+ // Before first open, hide offscreen to avoid flash
222
+ !this.hasBeenOpened() && (s === 'left' ? '-translate-x-full' : 'translate-x-full')
223
+ );
224
+ });
225
+
226
+ // Inner div - actual sidebar content
227
+ innerClasses = computed(() => {
228
+ const v = this.layout();
229
+ return cn(
230
+ 'group flex h-full w-full flex-col bg-sidebar',
231
+ !this.context.isMobile() && v === 'floating' && 'rounded-lg border border-sidebar-border shadow-sm'
232
+ );
233
+ });
234
+
235
+ constructor() {
236
+ // Ensure all template bindings are flushed after first render (zoneless CD fix)
237
+ afterNextRender(() => this.cdr.detectChanges());
238
+
239
+ // Body scroll lock when mobile drawer is open
240
+ effect(() => {
241
+ const isOpen = this.context.isMobile() && this.context.openMobile();
242
+ this.doc.body.style.overflow = isOpen ? 'hidden' : '';
243
+ });
244
+
245
+ this.destroyRef.onDestroy(() => {
246
+ this.doc.body.style.overflow = '';
247
+ });
248
+ }
249
+
250
+ protected onBackdropClick(): void {
251
+ this.context.setOpenMobile(false);
252
+ }
253
+
254
+ }
@@ -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,21 @@
1
+ // Menu tokens
2
+ export { SNG_MENU_PANEL, type MenuPanel } from './sng-menu-tokens';
3
+
4
+ // Menu components
5
+ export { SngMenu } from './sng-menu';
6
+ export { SngMenuTrigger } from './sng-menu-trigger';
7
+ export { SngContextTrigger } from './sng-context-trigger';
8
+ // Menu item components
9
+ export { SngMenuItem } from './sng-menu-item';
10
+ export { SngMenuSeparator } from './sng-menu-separator';
11
+ export { SngMenuLabel } from './sng-menu-label';
12
+ export { SngMenuShortcut } from './sng-menu-shortcut';
13
+ export { SngMenuCheckboxItem } from './sng-menu-checkbox-item';
14
+ export { SngMenuRadioGroup } from './sng-menu-radio-group';
15
+ export { SngMenuRadioItem } from './sng-menu-radio-item';
16
+
17
+ // Submenu components
18
+ export { SngMenuSub } from './sng-menu-sub';
19
+ export { SngMenuSubTrigger } from './sng-menu-sub-trigger';
20
+ export { SngMenuSubContent } from './sng-menu-sub-content';
21
+
@@ -0,0 +1,128 @@
1
+ import {
2
+ Directive,
3
+ inject,
4
+ Injector,
5
+ input,
6
+ OnDestroy,
7
+ ViewContainerRef,
8
+ afterNextRender,
9
+ } from '@angular/core';
10
+ import { Overlay, type OverlayRef, type ConnectedPosition } from '@angular/cdk/overlay';
11
+ import { TemplatePortal } from '@angular/cdk/portal';
12
+ import { SngMenu } from './sng-menu';
13
+ import { animateOverlayClose, focusMenuContent } from './sng-menu-tokens';
14
+
15
+ const CONTEXT_MENU_POSITIONS: ConnectedPosition[] = [
16
+ { originX: 'start', originY: 'bottom', overlayX: 'start', overlayY: 'top' },
17
+ { originX: 'start', originY: 'top', overlayX: 'start', overlayY: 'bottom' },
18
+ { originX: 'end', originY: 'bottom', overlayX: 'end', overlayY: 'top' },
19
+ { originX: 'end', originY: 'top', overlayX: 'end', overlayY: 'bottom' },
20
+ ];
21
+
22
+ /**
23
+ * Directive that opens a menu on right-click (context menu).
24
+ * Owns the CDK Overlay lifecycle for point-based positioning.
25
+ *
26
+ * @example
27
+ * ```html
28
+ * <div [sngContextTrigger]="myMenu" class="w-full h-64 border">
29
+ * Right click anywhere in this area
30
+ * </div>
31
+ * <sng-menu #myMenu>...</sng-menu>
32
+ * ```
33
+ */
34
+ @Directive({
35
+ selector: '[sngContextTrigger]',
36
+ standalone: true,
37
+ host: {
38
+ '[attr.data-state]': 'menu()?.isOpen() ? "open" : "closed"',
39
+ '(contextmenu)': 'onContextMenu($event)',
40
+ },
41
+ })
42
+ export class SngContextTrigger implements OnDestroy {
43
+ private overlay = inject(Overlay);
44
+ private injector = inject(Injector);
45
+ private viewContainerRef = inject(ViewContainerRef);
46
+
47
+ private overlayRef: OverlayRef | null = null;
48
+ private _closing = false;
49
+
50
+ /** The menu to open when right-clicked. */
51
+ menu = input.required<SngMenu>({ alias: 'sngContextTrigger' });
52
+
53
+ onContextMenu(event: MouseEvent) {
54
+ event.preventDefault();
55
+ event.stopPropagation();
56
+ this.openAt(event.clientX, event.clientY);
57
+ }
58
+
59
+ ngOnDestroy() {
60
+ this.close();
61
+ this.disposeOverlay();
62
+ }
63
+
64
+ private openAt(x: number, y: number) {
65
+ const menuRef = this.menu();
66
+ if (!menuRef) return;
67
+
68
+ this.disposeOverlay();
69
+
70
+ const template = menuRef._menuTemplate();
71
+ if (!template) return;
72
+
73
+ if (!Number.isFinite(x) || !Number.isFinite(y) || x < 0 || y < 0) return;
74
+
75
+ menuRef.currentSide.set('bottom');
76
+ menuRef._closeFromTrigger = () => this.close();
77
+
78
+ const positionStrategy = this.overlay.position()
79
+ .flexibleConnectedTo({ x, y })
80
+ .withPositions(CONTEXT_MENU_POSITIONS)
81
+ .withPush(true)
82
+ .withViewportMargin(8);
83
+
84
+ this.overlayRef = this.overlay.create({
85
+ positionStrategy,
86
+ scrollStrategy: this.overlay.scrollStrategies.close(),
87
+ hasBackdrop: true,
88
+ backdropClass: 'cdk-overlay-transparent-backdrop',
89
+ });
90
+
91
+ this.overlayRef.backdropClick().subscribe(() => this.close());
92
+ this.overlayRef.detachments().subscribe(() => this.close());
93
+
94
+ const portal = new TemplatePortal(template, this.viewContainerRef);
95
+ this.overlayRef.attach(portal);
96
+
97
+ menuRef.isOpen.set(true);
98
+
99
+ afterNextRender(() => {
100
+ const panel = this.overlayRef?.overlayElement.querySelector('[role="menu"]') as HTMLElement | null;
101
+ if (panel) {
102
+ focusMenuContent(panel);
103
+ }
104
+ }, { injector: this.injector });
105
+ }
106
+
107
+ private close() {
108
+ const menuRef = this.menu();
109
+ if (!menuRef?.isOpen() || this._closing) return;
110
+ this._closing = true;
111
+ menuRef._subContentOverlays.forEach(sc => sc._resetOnMenuClose());
112
+ menuRef.isOpen.set(false);
113
+
114
+ if (this.overlayRef) {
115
+ animateOverlayClose(this.overlayRef, () => this.disposeOverlay());
116
+ } else {
117
+ this._closing = false;
118
+ }
119
+ }
120
+
121
+ private disposeOverlay() {
122
+ this._closing = false;
123
+ if (this.overlayRef) {
124
+ this.overlayRef.dispose();
125
+ this.overlayRef = null;
126
+ }
127
+ }
128
+ }
@@ -0,0 +1,91 @@
1
+ import {
2
+ Component,
3
+ ChangeDetectionStrategy,
4
+ ViewEncapsulation,
5
+ input,
6
+ computed,
7
+ model,
8
+ booleanAttribute,
9
+ inject,
10
+ } from '@angular/core';
11
+ import { cn } from './cn';
12
+ import { SNG_MENU_PANEL, MENU_ITEM_BASE_CLASSES } from './sng-menu-tokens';
13
+
14
+ /**
15
+ * A checkbox menu item that can be toggled on/off.
16
+ *
17
+ * @example
18
+ * ```html
19
+ * <sng-menu>
20
+ * <sng-menu-checkbox-item [(checked)]="showToolbar">
21
+ * Show Toolbar
22
+ * </sng-menu-checkbox-item>
23
+ * </sng-menu>
24
+ * ```
25
+ */
26
+ @Component({
27
+ selector: 'sng-menu-checkbox-item',
28
+ standalone: true,
29
+ changeDetection: ChangeDetectionStrategy.OnPush,
30
+ encapsulation: ViewEncapsulation.None,
31
+ host: {
32
+ '[class]': 'hostClasses()',
33
+ '[attr.data-state]': 'checked() ? "checked" : "unchecked"',
34
+ '[attr.data-disabled]': 'resolvedDisabled() ? "" : null',
35
+ '[attr.aria-checked]': 'checked()',
36
+ '[attr.tabindex]': 'resolvedDisabled() ? -1 : 0',
37
+ 'role': 'menuitemcheckbox',
38
+ '(click)': 'toggle()',
39
+ },
40
+ template: `
41
+ <span class="flex h-4 w-4 items-center justify-center mr-2">
42
+ @if (checked()) {
43
+ <svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
44
+ <polyline points="20 6 9 17 4 12" />
45
+ </svg>
46
+ }
47
+ </span>
48
+ <ng-content />
49
+ `,
50
+ })
51
+ export class SngMenuCheckboxItem {
52
+ private panel = inject(SNG_MENU_PANEL, { optional: true });
53
+
54
+ /** Custom CSS classes. */
55
+ class = input<string>('');
56
+
57
+ /** Whether the checkbox is checked. Supports two-way binding. */
58
+ checked = model(false);
59
+
60
+ /** Whether the checkbox 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 toggled. 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
+ hostClasses = computed(() =>
75
+ cn(
76
+ ...MENU_ITEM_BASE_CLASSES,
77
+ this.class()
78
+ )
79
+ );
80
+
81
+ /** Toggles the checked state. */
82
+ toggle() {
83
+ if (!this.resolvedDisabled()) {
84
+ this.checked.update(v => !v);
85
+ const shouldClose = this.isCloseOnSelect() ?? this.panel?.closeOnSelect() ?? true;
86
+ if (shouldClose) {
87
+ this.panel?.close();
88
+ }
89
+ }
90
+ }
91
+ }
@@ -0,0 +1,80 @@
1
+ import {
2
+ Directive,
3
+ ElementRef,
4
+ inject,
5
+ input,
6
+ computed,
7
+ booleanAttribute,
8
+ } from '@angular/core';
9
+ import { cn } from './cn';
10
+ import { SNG_MENU_PANEL, MENU_ITEM_BASE_CLASSES } from './sng-menu-tokens';
11
+
12
+ /**
13
+ * A standard interactive menu item.
14
+ *
15
+ * @example
16
+ * ```html
17
+ * <sng-menu>
18
+ * <sng-menu-item>Edit</sng-menu-item>
19
+ * <sng-menu-item [isDisabled]="true">Delete</sng-menu-item>
20
+ * </sng-menu>
21
+ * ```
22
+ */
23
+ @Directive({
24
+ selector: 'sng-menu-item',
25
+ standalone: true,
26
+ host: {
27
+ '[class]': 'hostClasses()',
28
+ '[attr.data-disabled]': 'resolvedDisabled() ? "" : null',
29
+ '[attr.tabindex]': 'resolvedDisabled() ? -1 : 0',
30
+ 'role': 'menuitem',
31
+ '(click)': 'onClick()',
32
+ },
33
+ })
34
+ export class SngMenuItem {
35
+ private elementRef = inject(ElementRef);
36
+ private panel = inject(SNG_MENU_PANEL, { optional: true });
37
+
38
+ /** Custom CSS classes. */
39
+ class = input<string>('');
40
+
41
+ /** Whether the menu item is disabled. */
42
+ disabled = input(false, { transform: booleanAttribute });
43
+
44
+ /** Legacy disabled input name. */
45
+ isDisabled = input<unknown>(undefined);
46
+
47
+ /** Whether to add left padding for alignment with checkbox/radio items. */
48
+ inset = input(false, { transform: booleanAttribute });
49
+
50
+ /** Whether to close the menu when this item is selected. Uses parent menu's setting when not specified. */
51
+ isCloseOnSelect = input<boolean | undefined>(undefined);
52
+
53
+ resolvedDisabled = computed(() => {
54
+ const legacyValue = this.isDisabled();
55
+ return legacyValue === undefined ? this.disabled() : booleanAttribute(legacyValue);
56
+ });
57
+
58
+ hostClasses = computed(() =>
59
+ cn(
60
+ ...MENU_ITEM_BASE_CLASSES,
61
+ this.inset() && 'pl-8',
62
+ this.class()
63
+ )
64
+ );
65
+
66
+ /** CDK FocusableOption — focus this item. */
67
+ focus(): void {
68
+ this.elementRef.nativeElement.focus();
69
+ }
70
+
71
+ onClick() {
72
+ if (!this.resolvedDisabled()) {
73
+ const shouldClose = this.isCloseOnSelect() ?? this.panel?.closeOnSelect() ?? true;
74
+ if (shouldClose) {
75
+ this.panel?.close();
76
+ }
77
+ }
78
+ }
79
+
80
+ }