@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,65 @@
1
+ import { Component, ChangeDetectionStrategy, ViewEncapsulation, input, computed } from '@angular/core';
2
+ import { cn } from './cn';
3
+
4
+ /**
5
+ * Progress bar for displaying completion status.
6
+ * Uses semantic `role="progressbar"` with proper ARIA attributes.
7
+ * Supports horizontal and vertical orientations.
8
+ *
9
+ * @example
10
+ * ```html
11
+ * <sng-progress [value]="33" />
12
+ * <sng-progress [value]="66" orientation="vertical" class="h-48" />
13
+ * ```
14
+ */
15
+ @Component({
16
+ selector: 'sng-progress',
17
+ standalone: true,
18
+ changeDetection: ChangeDetectionStrategy.OnPush,
19
+ encapsulation: ViewEncapsulation.None,
20
+ host: {
21
+ 'role': 'progressbar',
22
+ '[attr.aria-valuemin]': '0',
23
+ '[attr.aria-valuemax]': '100',
24
+ '[attr.aria-valuenow]': 'clampedValue()',
25
+ '[attr.aria-orientation]': 'orientation()',
26
+ '[class]': 'hostClasses()',
27
+ },
28
+ template: `
29
+ <div
30
+ [class]="indicatorClasses()"
31
+ [style.transform]="indicatorTransform()">
32
+ </div>
33
+ `,
34
+ })
35
+ export class SngProgress {
36
+ /** Custom CSS classes. */
37
+ class = input<string>('');
38
+
39
+ /** Current progress value (0-100). */
40
+ value = input<number>(0);
41
+
42
+ /** Orientation of the progress bar. */
43
+ orientation = input<'horizontal' | 'vertical'>('horizontal');
44
+
45
+ hostClasses = computed(() =>
46
+ cn(
47
+ 'relative block overflow-hidden rounded-full bg-current/20 text-primary',
48
+ this.orientation() === 'vertical' ? 'w-2 h-full' : 'h-2 w-full',
49
+ this.class()
50
+ )
51
+ );
52
+
53
+ indicatorClasses = computed(() =>
54
+ cn('h-full w-full flex-1 bg-current transition-transform')
55
+ );
56
+
57
+ clampedValue = computed(() => Math.max(0, Math.min(100, this.value())));
58
+
59
+ indicatorTransform = computed(() => {
60
+ const remaining = 100 - this.clampedValue();
61
+ return this.orientation() === 'vertical'
62
+ ? `translateY(${remaining}%)`
63
+ : `translateX(-${remaining}%)`;
64
+ });
65
+ }
@@ -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,5 @@
1
+ export { SngRadio } from './sng-radio';
2
+ export { SngRadioItem } from './sng-radio-item';
3
+
4
+ // Backwards compatibility aliases (deprecated)
5
+ export { SngRadio as SngRadioGroup } from './sng-radio';
@@ -0,0 +1,100 @@
1
+ import {
2
+ Component,
3
+ computed,
4
+ input,
5
+ inject,
6
+ ChangeDetectionStrategy,
7
+ ViewEncapsulation,
8
+ booleanAttribute,
9
+ forwardRef,
10
+ } from '@angular/core';
11
+ import { SngRadio } from './sng-radio';
12
+ import { cn } from './cn';
13
+
14
+ const TRANSITION_SHADOW = 'transition-[color,box-shadow] outline-none';
15
+ const FOCUS_RING = 'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]';
16
+
17
+ /**
18
+ * An individual radio button within a radio group.
19
+ * Must be used inside a `sng-radio` container.
20
+ *
21
+ * @example
22
+ * ```html
23
+ * <sng-radio [(value)]="selected">
24
+ * <sng-radio-item value="option1">Option 1</sng-radio-item>
25
+ * <sng-radio-item value="option2">Option 2</sng-radio-item>
26
+ * </sng-radio>
27
+ * ```
28
+ */
29
+ @Component({
30
+ selector: 'sng-radio-item',
31
+ standalone: true,
32
+ changeDetection: ChangeDetectionStrategy.OnPush,
33
+ encapsulation: ViewEncapsulation.None,
34
+ host: {
35
+ '[class]': 'hostClasses()',
36
+ '(click)': 'select()',
37
+ '[attr.role]': '"radio"',
38
+ '[attr.aria-checked]': 'isChecked()',
39
+ '[attr.aria-disabled]': 'isDisabled()',
40
+ '[attr.tabindex]': '_tabindex()',
41
+ '[attr.data-state]': 'isChecked() ? "checked" : "unchecked"',
42
+ },
43
+ template: `
44
+ @if (isChecked()) {
45
+ <svg
46
+ [class]="dotClasses()"
47
+ viewBox="0 0 24 24"
48
+ xmlns="http://www.w3.org/2000/svg"
49
+ >
50
+ <circle cx="12" cy="12" r="12"/>
51
+ </svg>
52
+ }
53
+ `,
54
+ })
55
+ export class SngRadioItem {
56
+ private group = inject(forwardRef(() => SngRadio), { optional: true });
57
+
58
+ /** Custom CSS classes. */
59
+ class = input<string>('');
60
+
61
+ /** The value associated with this radio item. */
62
+ value = input.required<string>();
63
+
64
+ /** Whether this radio item is disabled. */
65
+ disabled = input(false, { transform: booleanAttribute });
66
+
67
+ isDisabled = computed(() => this.disabled() || (this.group?.disabled() ?? false));
68
+
69
+ isChecked = computed(() => {
70
+ if (!this.group) return false;
71
+ return this.group.value() === this.value();
72
+ });
73
+
74
+ /** @internal */
75
+ _tabindex = computed(() => {
76
+ return this.isDisabled() ? -1 : 0;
77
+ });
78
+
79
+ hostClasses = computed(() =>
80
+ cn(
81
+ 'aspect-square shrink-0 rounded-full border shadow-xs',
82
+ 'size-4',
83
+ 'cursor-pointer flex items-center justify-center text-primary',
84
+ TRANSITION_SHADOW,
85
+ FOCUS_RING,
86
+ 'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
87
+ 'aria-disabled:cursor-not-allowed aria-disabled:opacity-50',
88
+ 'border-input dark:bg-input/30',
89
+ this.class()
90
+ )
91
+ );
92
+
93
+ /** @internal */
94
+ dotClasses = computed(() => cn('fill-primary', 'size-1/2'));
95
+
96
+ select() {
97
+ if (this.isDisabled()) return;
98
+ this.group?._selectValue(this.value());
99
+ }
100
+ }
@@ -0,0 +1,54 @@
1
+ import {
2
+ Component,
3
+ model,
4
+ computed,
5
+ ChangeDetectionStrategy,
6
+ input,
7
+ booleanAttribute,
8
+ } from '@angular/core';
9
+ import { cn } from './cn';
10
+
11
+ /**
12
+ * A radio group container that manages selection state for radio items.
13
+ * Uses Angular's model() for two-way binding, compatible with Signal Forms.
14
+ *
15
+ * @example
16
+ * ```html
17
+ * <sng-radio [(value)]="selectedPlan">
18
+ * <sng-radio-item value="free">Free</sng-radio-item>
19
+ * <sng-radio-item value="pro">Pro</sng-radio-item>
20
+ * </sng-radio>
21
+ * ```
22
+ */
23
+ @Component({
24
+ selector: 'sng-radio',
25
+ standalone: true,
26
+ changeDetection: ChangeDetectionStrategy.OnPush,
27
+ host: {
28
+ '[attr.role]': '"radiogroup"',
29
+ '[attr.aria-disabled]': 'disabled()',
30
+ '[class]': 'hostClasses()',
31
+ },
32
+ template: `<ng-content />`,
33
+ })
34
+ export class SngRadio {
35
+ /** Custom CSS classes. */
36
+ class = input<string>('');
37
+
38
+ /** The selected value. Supports two-way binding. */
39
+ value = model<string>('');
40
+
41
+ /** Name attribute for the radio group. */
42
+ name = input<string>('');
43
+
44
+ /** Whether the entire radio group is disabled. */
45
+ disabled = input(false, { transform: booleanAttribute });
46
+
47
+ hostClasses = computed(() => cn('grid gap-3', this.class()));
48
+
49
+ /** @internal Called by radio items when selected. */
50
+ _selectValue(value: string) {
51
+ if (this.disabled()) return;
52
+ this.value.set(value);
53
+ }
54
+ }
@@ -0,0 +1,6 @@
1
+ import { clsx, type ClassValue } from 'clsx';
2
+ import { twMerge } from 'tailwind-merge';
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs));
6
+ }
@@ -0,0 +1,3 @@
1
+ export { SngResizableGroup, type ResizableDirection } from './sng-resizable-group';
2
+ export { SngResizablePanel } from './sng-resizable-panel';
3
+ export { SngResizableHandle } from './sng-resizable-handle';
@@ -0,0 +1,188 @@
1
+ import {
2
+ Component,
3
+ ChangeDetectionStrategy,
4
+ input,
5
+ computed,
6
+ contentChildren,
7
+ signal,
8
+ effect,
9
+ untracked,
10
+ AfterContentInit,
11
+ ElementRef,
12
+ inject,
13
+ PLATFORM_ID,
14
+ } from '@angular/core';
15
+ import { isPlatformBrowser } from '@angular/common';
16
+ import { cn } from './cn';
17
+ import { SngResizablePanel } from './sng-resizable-panel';
18
+
19
+ export type ResizableDirection = 'horizontal' | 'vertical';
20
+
21
+ /**
22
+ * A container component that manages a group of resizable panels.
23
+ *
24
+ * @example
25
+ * ```html
26
+ * <sng-resizable-group direction="horizontal" class="min-h-[200px] max-w-md rounded-lg border">
27
+ * <sng-resizable-panel [defaultSize]="50">
28
+ * <div class="flex h-full items-center justify-center p-6">One</div>
29
+ * </sng-resizable-panel>
30
+ * <sng-resizable-handle [withHandle]="true" />
31
+ * <sng-resizable-panel [defaultSize]="50">
32
+ * <div class="flex h-full items-center justify-center p-6">Two</div>
33
+ * </sng-resizable-panel>
34
+ * </sng-resizable-group>
35
+ * ```
36
+ */
37
+ @Component({
38
+ selector: 'sng-resizable-group',
39
+ standalone: true,
40
+ changeDetection: ChangeDetectionStrategy.OnPush,
41
+ host: {
42
+ '[class]': 'hostClasses()',
43
+ '[attr.data-panel-group]': '"true"',
44
+ '[attr.data-direction]': 'direction()',
45
+ },
46
+ template: `<ng-content />`,
47
+ })
48
+ export class SngResizableGroup implements AfterContentInit {
49
+ private elementRef = inject(ElementRef);
50
+ private platformId = inject(PLATFORM_ID);
51
+ private _lastPanelCount = 0;
52
+
53
+ /** The direction in which the panels are laid out. */
54
+ direction = input<ResizableDirection>('horizontal');
55
+
56
+ /** Custom CSS classes. */
57
+ class = input<string>('');
58
+
59
+ /** @internal */
60
+ _panels = contentChildren(SngResizablePanel);
61
+
62
+ /** @internal */
63
+ _panelSizes = signal<number[]>([]);
64
+
65
+ hostClasses = computed(() =>
66
+ cn(
67
+ 'flex h-full w-full',
68
+ this.direction() === 'horizontal' ? 'flex-row' : 'flex-col',
69
+ this.class()
70
+ )
71
+ );
72
+
73
+ constructor() {
74
+ // Re-initialize when panels are added or removed dynamically (e.g. via @if)
75
+ effect(() => {
76
+ const count = this._panels().length;
77
+ if (this._lastPanelCount > 0 && count !== this._lastPanelCount) {
78
+ this._lastPanelCount = count;
79
+ untracked(() => this.initializePanelSizes());
80
+ }
81
+ });
82
+ }
83
+
84
+ ngAfterContentInit() {
85
+ this._lastPanelCount = this._panels().length;
86
+ this.initializePanelSizes();
87
+ }
88
+
89
+ /** @internal Returns the container size in pixels along the layout axis. */
90
+ _getContainerSize(): number {
91
+ const el = this.elementRef.nativeElement;
92
+ return this.direction() === 'horizontal' ? el.offsetWidth : el.offsetHeight;
93
+ }
94
+
95
+ private initializePanelSizes() {
96
+ const panelArray = this._panels();
97
+ if (panelArray.length === 0) return;
98
+
99
+ const sizes: number[] = [];
100
+ let totalDefault = 0;
101
+ let panelsWithoutDefault = 0;
102
+
103
+ // First pass: collect default sizes
104
+ panelArray.forEach((panel) => {
105
+ const defaultSize = panel.defaultSize();
106
+ if (defaultSize !== undefined) {
107
+ sizes.push(defaultSize);
108
+ totalDefault += defaultSize;
109
+ } else {
110
+ sizes.push(-1); // Mark for later
111
+ panelsWithoutDefault++;
112
+ }
113
+ });
114
+
115
+ // Second pass: distribute remaining space
116
+ if (panelsWithoutDefault > 0) {
117
+ const remainingSpace = 100 - totalDefault;
118
+ const sizePerPanel = remainingSpace / panelsWithoutDefault;
119
+ for (let i = 0; i < sizes.length; i++) {
120
+ if (sizes[i] === -1) {
121
+ sizes[i] = sizePerPanel;
122
+ }
123
+ }
124
+ }
125
+
126
+ this._panelSizes.set(sizes);
127
+
128
+ panelArray.forEach((panel, index) => {
129
+ panel._setSize(sizes[index]);
130
+ });
131
+ }
132
+
133
+ /** @internal Called by handles to resize adjacent panels. */
134
+ _resizePanel(panelIndex: number, delta: number) {
135
+ if (!isPlatformBrowser(this.platformId)) return;
136
+
137
+ const sizes = [...this._panelSizes()];
138
+ const panelArray = this._panels();
139
+
140
+ // Safety checks
141
+ if (panelIndex < 0 || panelIndex >= panelArray.length - 1) return;
142
+ if (sizes.length !== panelArray.length) return;
143
+
144
+ const currentPanel = panelArray[panelIndex];
145
+ const nextPanel = panelArray[panelIndex + 1];
146
+ if (!currentPanel || !nextPanel) return;
147
+
148
+ const containerSize = this._getContainerSize();
149
+
150
+ // Convert pixel delta to percentage
151
+ const deltaPercent = (delta / containerSize) * 100;
152
+
153
+ const currentMin = currentPanel.minSize() ?? 0;
154
+ const currentMax = currentPanel.maxSize() ?? 100;
155
+ const nextMin = nextPanel.minSize() ?? 0;
156
+ const nextMax = nextPanel.maxSize() ?? 100;
157
+
158
+ // Calculate new sizes
159
+ let newCurrentSize = sizes[panelIndex] + deltaPercent;
160
+ let newNextSize = sizes[panelIndex + 1] - deltaPercent;
161
+
162
+ // Apply constraints
163
+ if (newCurrentSize < currentMin) {
164
+ newCurrentSize = currentMin;
165
+ newNextSize = sizes[panelIndex] + sizes[panelIndex + 1] - currentMin;
166
+ } else if (newCurrentSize > currentMax) {
167
+ newCurrentSize = currentMax;
168
+ newNextSize = sizes[panelIndex] + sizes[panelIndex + 1] - currentMax;
169
+ }
170
+
171
+ if (newNextSize < nextMin) {
172
+ newNextSize = nextMin;
173
+ newCurrentSize = sizes[panelIndex] + sizes[panelIndex + 1] - nextMin;
174
+ } else if (newNextSize > nextMax) {
175
+ newNextSize = nextMax;
176
+ newCurrentSize = sizes[panelIndex] + sizes[panelIndex + 1] - nextMax;
177
+ }
178
+
179
+ sizes[panelIndex] = newCurrentSize;
180
+ sizes[panelIndex + 1] = newNextSize;
181
+
182
+ this._panelSizes.set(sizes);
183
+
184
+ // Update panels
185
+ currentPanel._setSize(newCurrentSize);
186
+ nextPanel._setSize(newNextSize);
187
+ }
188
+ }
@@ -0,0 +1,236 @@
1
+ import {
2
+ Component,
3
+ ChangeDetectionStrategy,
4
+ ViewEncapsulation,
5
+ input,
6
+ computed,
7
+ inject,
8
+ ElementRef,
9
+ PLATFORM_ID,
10
+ OnInit,
11
+ OnDestroy,
12
+ signal,
13
+ booleanAttribute,
14
+ } from '@angular/core';
15
+ import { isPlatformBrowser, DOCUMENT } from '@angular/common';
16
+ import { cn } from './cn';
17
+ import { SngResizableGroup } from './sng-resizable-group';
18
+
19
+ const FOCUS_RING = 'focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1';
20
+
21
+ /**
22
+ * A draggable handle for resizing adjacent panels within a resizable group.
23
+ * Supports mouse and touch interactions.
24
+ *
25
+ * @example
26
+ * ```html
27
+ * <sng-resizable-group direction="horizontal" class="min-h-[200px] rounded-lg border">
28
+ * <sng-resizable-panel [defaultSize]="50">Left panel</sng-resizable-panel>
29
+ * <sng-resizable-handle [withHandle]="true" />
30
+ * <sng-resizable-panel [defaultSize]="50">Right panel</sng-resizable-panel>
31
+ * </sng-resizable-group>
32
+ * ```
33
+ */
34
+ @Component({
35
+ selector: 'sng-resizable-handle',
36
+ standalone: true,
37
+ changeDetection: ChangeDetectionStrategy.OnPush,
38
+ encapsulation: ViewEncapsulation.None,
39
+ host: {
40
+ '[class]': 'hostClasses()',
41
+ '[attr.data-panel-resize-handle]': '"true"',
42
+ '[attr.data-direction]': 'direction()',
43
+ '[attr.data-dragging]': 'isDragging()',
44
+ '[attr.tabindex]': '"0"',
45
+ '[attr.role]': '"separator"',
46
+ '[attr.aria-orientation]': 'direction() === "horizontal" ? "vertical" : "horizontal"',
47
+ '[attr.aria-valuenow]': '_ariaValueNow()',
48
+ '[attr.aria-valuemin]': '_ariaValueMin()',
49
+ '[attr.aria-valuemax]': '_ariaValueMax()',
50
+ '(mousedown)': 'onMouseDown($event)',
51
+ '(touchstart)': 'onTouchStart($event)',
52
+ },
53
+ template: `
54
+ @if (withHandle()) {
55
+ <div [class]="gripClasses()">
56
+ @if (direction() === 'horizontal') {
57
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
58
+ <circle cx="9" cy="12" r="1"/><circle cx="9" cy="5" r="1"/><circle cx="9" cy="19" r="1"/>
59
+ <circle cx="15" cy="12" r="1"/><circle cx="15" cy="5" r="1"/><circle cx="15" cy="19" r="1"/>
60
+ </svg>
61
+ } @else {
62
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
63
+ <circle cx="12" cy="9" r="1"/><circle cx="5" cy="9" r="1"/><circle cx="19" cy="9" r="1"/>
64
+ <circle cx="12" cy="15" r="1"/><circle cx="5" cy="15" r="1"/><circle cx="19" cy="15" r="1"/>
65
+ </svg>
66
+ }
67
+ </div>
68
+ }
69
+ `,
70
+ })
71
+ export class SngResizableHandle implements OnInit, OnDestroy {
72
+ private group = inject(SngResizableGroup, { optional: true });
73
+ private elementRef = inject(ElementRef);
74
+ private document = inject(DOCUMENT);
75
+ private platformId = inject(PLATFORM_ID);
76
+
77
+ /** Whether to display a visible grip icon on the handle. */
78
+ withHandle = input(false, { transform: booleanAttribute });
79
+
80
+ /** Custom CSS classes. */
81
+ class = input<string>('');
82
+
83
+ isDragging = signal(false);
84
+
85
+ private _panelIndex = signal(0);
86
+ private startPosition = 0;
87
+
88
+ direction = computed(() => this.group?.direction() ?? 'horizontal');
89
+
90
+ /** @internal */
91
+ _ariaValueNow = computed(() => {
92
+ if (!this.group) return 0;
93
+ const sizes = this.group._panelSizes();
94
+ const index = this._panelIndex();
95
+ return Math.round(sizes[index] ?? 0);
96
+ });
97
+
98
+ /** @internal */
99
+ _ariaValueMin = computed(() => {
100
+ if (!this.group) return 0;
101
+ const panels = this.group._panels();
102
+ const index = this._panelIndex();
103
+ return panels[index]?.minSize() ?? 0;
104
+ });
105
+
106
+ /** @internal */
107
+ _ariaValueMax = computed(() => {
108
+ if (!this.group) return 100;
109
+ const panels = this.group._panels();
110
+ const index = this._panelIndex();
111
+ return panels[index]?.maxSize() ?? 100;
112
+ });
113
+
114
+ hostClasses = computed(() =>
115
+ cn(
116
+ 'relative flex items-center justify-center bg-border',
117
+ FOCUS_RING,
118
+ this.direction() === 'horizontal'
119
+ ? 'h-full w-px cursor-col-resize after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2'
120
+ : 'h-px w-full cursor-row-resize after:absolute after:inset-x-0 after:top-1/2 after:h-1 after:-translate-y-1/2',
121
+ 'data-[dragging=true]:bg-primary',
122
+ this.class()
123
+ )
124
+ );
125
+
126
+ gripClasses = computed(() =>
127
+ cn(
128
+ 'z-10 flex items-center justify-center rounded-sm border bg-border',
129
+ this.direction() === 'horizontal' ? 'h-4 w-3' : 'h-3 w-4'
130
+ )
131
+ );
132
+
133
+ ngOnInit() {
134
+ this._calculatePanelIndex();
135
+ }
136
+
137
+ ngOnDestroy() {
138
+ this.removeListeners();
139
+ }
140
+
141
+ private _calculatePanelIndex() {
142
+ if (!this.group || !isPlatformBrowser(this.platformId)) return;
143
+
144
+ const element = this.elementRef.nativeElement;
145
+ const parent = element.parentElement;
146
+ if (!parent) return;
147
+
148
+ // Count panels before this handle
149
+ const children = Array.from(parent.children) as Element[];
150
+ let panelCount = 0;
151
+ for (const child of children) {
152
+ if (child === element) break;
153
+ if (child.hasAttribute('data-panel')) {
154
+ panelCount++;
155
+ }
156
+ }
157
+ // panelIndex is the index of the panel to the LEFT/TOP of this handle
158
+ this._panelIndex.set(Math.max(0, panelCount - 1));
159
+ }
160
+
161
+ onMouseDown(event: MouseEvent) {
162
+ event.preventDefault();
163
+ this.startDrag(event.clientX, event.clientY);
164
+
165
+ if (isPlatformBrowser(this.platformId)) {
166
+ this.document.addEventListener('mousemove', this.onMouseMove);
167
+ this.document.addEventListener('mouseup', this.onMouseUp);
168
+ }
169
+ }
170
+
171
+ onTouchStart(event: TouchEvent) {
172
+ if (event.touches.length !== 1) return;
173
+ const touch = event.touches[0];
174
+ this.startDrag(touch.clientX, touch.clientY);
175
+
176
+ if (isPlatformBrowser(this.platformId)) {
177
+ this.document.addEventListener('touchmove', this.onTouchMove, { passive: false });
178
+ this.document.addEventListener('touchend', this.onTouchEnd);
179
+ }
180
+ }
181
+
182
+ private startDrag(clientX: number, clientY: number) {
183
+ this.isDragging.set(true);
184
+ this._calculatePanelIndex();
185
+ this.startPosition =
186
+ this.direction() === 'horizontal' ? clientX : clientY;
187
+ }
188
+
189
+ private onMouseMove = (event: MouseEvent) => {
190
+ this.handleMove(event.clientX, event.clientY);
191
+ };
192
+
193
+ private onTouchMove = (event: TouchEvent) => {
194
+ event.preventDefault();
195
+ if (event.touches.length !== 1) return;
196
+ const touch = event.touches[0];
197
+ this.handleMove(touch.clientX, touch.clientY);
198
+ };
199
+
200
+ private handleMove(clientX: number, clientY: number) {
201
+ const currentPosition =
202
+ this.direction() === 'horizontal' ? clientX : clientY;
203
+ const delta = currentPosition - this.startPosition;
204
+ this.startPosition = currentPosition;
205
+
206
+ if (this.group && delta !== 0) {
207
+ this.group._resizePanel(this._panelIndex(), delta);
208
+ }
209
+ }
210
+
211
+ private onMouseUp = () => {
212
+ this.endDrag();
213
+ this.document.removeEventListener('mousemove', this.onMouseMove);
214
+ this.document.removeEventListener('mouseup', this.onMouseUp);
215
+ };
216
+
217
+ private onTouchEnd = () => {
218
+ this.endDrag();
219
+ this.document.removeEventListener('touchmove', this.onTouchMove);
220
+ this.document.removeEventListener('touchend', this.onTouchEnd);
221
+ };
222
+
223
+ private endDrag() {
224
+ this.isDragging.set(false);
225
+ }
226
+
227
+ private removeListeners() {
228
+ if (isPlatformBrowser(this.platformId)) {
229
+ this.document.removeEventListener('mousemove', this.onMouseMove);
230
+ this.document.removeEventListener('mouseup', this.onMouseUp);
231
+ this.document.removeEventListener('touchmove', this.onTouchMove);
232
+ this.document.removeEventListener('touchend', this.onTouchEnd);
233
+ }
234
+ }
235
+
236
+ }