@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,288 @@
1
+ import {
2
+ Component,
3
+ ChangeDetectionStrategy,
4
+ ViewEncapsulation,
5
+ input,
6
+ model,
7
+ signal,
8
+ computed,
9
+ viewChild,
10
+ ElementRef,
11
+ booleanAttribute,
12
+ } from '@angular/core';
13
+ import { cn } from './cn';
14
+
15
+ /**
16
+ * File input component with button and dropzone modes.
17
+ *
18
+ * @example
19
+ * ```html
20
+ * <sng-file-input [(files)]="selectedFiles" />
21
+ * <sng-file-input dropzone multiple accept="image/*" />
22
+ * ```
23
+ */
24
+ @Component({
25
+ selector: 'sng-file-input',
26
+ standalone: true,
27
+ changeDetection: ChangeDetectionStrategy.OnPush,
28
+ encapsulation: ViewEncapsulation.None,
29
+ host: {
30
+ '[class]': 'hostClasses()',
31
+ },
32
+ template: `
33
+ <input
34
+ #fileInputRef
35
+ type="file"
36
+ class="sr-only"
37
+ [accept]="accept()"
38
+ [multiple]="multiple()"
39
+ [disabled]="disabled()"
40
+ (change)="onFileChange($event)"
41
+ />
42
+
43
+ @if (dropzone()) {
44
+ <!-- Dropzone mode -->
45
+ <div
46
+ [class]="dropzoneClasses()"
47
+ tabindex="0"
48
+ role="button"
49
+ [attr.aria-label]="'Upload files' + (accept() ? ', accepts: ' + accept() : '')"
50
+ (click)="openFilePicker()"
51
+ (dragover)="onDragOver($event)"
52
+ (dragleave)="onDragLeave($event)"
53
+ (drop)="onDrop($event)"
54
+ >
55
+ <svg
56
+ class="h-10 w-10 text-muted-foreground"
57
+ viewBox="0 0 24 24"
58
+ fill="none"
59
+ stroke="currentColor"
60
+ stroke-width="1.5"
61
+ stroke-linecap="round"
62
+ stroke-linejoin="round"
63
+ >
64
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
65
+ <polyline points="17 8 12 3 7 8" />
66
+ <line x1="12" y1="3" x2="12" y2="15" />
67
+ </svg>
68
+ <div class="text-center">
69
+ <p class="text-sm font-medium text-foreground">
70
+ Drag & drop files here
71
+ </p>
72
+ <p class="text-xs text-muted-foreground">
73
+ or click to browse
74
+ </p>
75
+ </div>
76
+ @if (accept()) {
77
+ <p class="text-xs text-muted-foreground">
78
+ Accepted: {{ accept() }}
79
+ </p>
80
+ }
81
+ </div>
82
+ } @else {
83
+ <!-- Button mode -->
84
+ <div [class]="buttonModeClasses()">
85
+ <button
86
+ type="button"
87
+ class="h-full rounded-l-md border-r border-border bg-muted px-3 text-sm font-medium text-foreground transition-colors hover:bg-muted/80"
88
+ [disabled]="disabled()"
89
+ (click)="openFilePicker()"
90
+ >
91
+ Choose File
92
+ </button>
93
+ <span class="flex-1 truncate px-3 text-muted-foreground">
94
+ {{ fileNameDisplay() }}
95
+ </span>
96
+ </div>
97
+ }
98
+
99
+ <!-- Selected files list -->
100
+ @if (files().length > 0 && showFileList()) {
101
+ <div class="mt-2 space-y-1">
102
+ @for (file of files(); track file.name) {
103
+ <div
104
+ class="flex items-center justify-between rounded-md border border-border bg-muted/50 px-3 py-1.5 text-sm"
105
+ >
106
+ <span class="truncate text-foreground">{{ file.name }}</span>
107
+ <button
108
+ type="button"
109
+ class="ml-2 text-muted-foreground transition-colors hover:text-foreground"
110
+ [disabled]="disabled()"
111
+ (click)="removeFile(file)"
112
+ >
113
+ <svg
114
+ class="h-4 w-4"
115
+ viewBox="0 0 24 24"
116
+ fill="none"
117
+ stroke="currentColor"
118
+ stroke-width="2"
119
+ stroke-linecap="round"
120
+ stroke-linejoin="round"
121
+ >
122
+ <line x1="18" y1="6" x2="6" y2="18" />
123
+ <line x1="6" y1="6" x2="18" y2="18" />
124
+ </svg>
125
+ </button>
126
+ </div>
127
+ }
128
+ </div>
129
+ }
130
+ `,
131
+ })
132
+ export class SngFileInput {
133
+ private fileInputRef = viewChild<ElementRef<HTMLInputElement>>('fileInputRef');
134
+
135
+ /** Whether the input is disabled. */
136
+ disabled = input(false, { transform: booleanAttribute });
137
+
138
+ /** Whether to show dropzone mode. */
139
+ dropzone = input(false, { transform: booleanAttribute });
140
+
141
+ /** Whether multiple files can be selected. */
142
+ multiple = input(false, { transform: booleanAttribute });
143
+
144
+ /** Accepted file types (e.g., '.jpg,.png' or 'image/*'). */
145
+ accept = input<string>('');
146
+
147
+ /** Whether to show the file list below the input. */
148
+ showFileList = input(true, { transform: booleanAttribute });
149
+
150
+ /** Custom CSS classes. */
151
+ class = input<string>('');
152
+
153
+ /** Selected files. Supports two-way binding via [(files)]. */
154
+ files = model<File[]>([]);
155
+
156
+ /** @internal */
157
+ _isDragging = signal(false);
158
+
159
+ /** @internal */
160
+ hostClasses = computed(() => cn('block w-full', this.class()));
161
+
162
+ /** @internal */
163
+ dropzoneClasses = computed(() =>
164
+ cn(
165
+ 'flex flex-col items-center justify-center gap-2 rounded-lg border-2 border-dashed p-6 transition-colors',
166
+ this._isDragging() ? 'border-primary bg-muted/50' : 'border-muted-foreground/25',
167
+ this.disabled() ? 'cursor-not-allowed opacity-50' : 'cursor-pointer'
168
+ )
169
+ );
170
+
171
+ /** @internal */
172
+ buttonModeClasses = computed(() =>
173
+ cn(
174
+ 'flex h-9 items-center rounded-md border border-border bg-background text-sm shadow-sm transition-colors',
175
+ this.disabled() ? 'cursor-not-allowed opacity-50' : ''
176
+ )
177
+ );
178
+
179
+ /** @internal */
180
+ fileNameDisplay = computed(() => {
181
+ const fileList = this.files();
182
+ if (fileList.length === 0) return 'No file chosen';
183
+ if (fileList.length === 1) return fileList[0].name;
184
+ return `${fileList.length} files selected`;
185
+ });
186
+
187
+ /** Open the native file picker dialog. */
188
+ openFilePicker(): void {
189
+ if (this.disabled()) return;
190
+ this.fileInputRef()?.nativeElement.click();
191
+ }
192
+
193
+ /** @internal */
194
+ onFileChange(event: Event): void {
195
+ const input = event.target as HTMLInputElement;
196
+ if (input.files) {
197
+ const newFiles = Array.from(input.files);
198
+ this.files.set(newFiles);
199
+ }
200
+ }
201
+
202
+ /** @internal */
203
+ onDragOver(event: DragEvent): void {
204
+ event.preventDefault();
205
+ event.stopPropagation();
206
+ if (!this.disabled()) {
207
+ this._isDragging.set(true);
208
+ }
209
+ }
210
+
211
+ /** @internal */
212
+ onDragLeave(event: DragEvent): void {
213
+ event.preventDefault();
214
+ event.stopPropagation();
215
+ this._isDragging.set(false);
216
+ }
217
+
218
+ /** @internal */
219
+ onDrop(event: DragEvent): void {
220
+ event.preventDefault();
221
+ event.stopPropagation();
222
+ this._isDragging.set(false);
223
+
224
+ if (this.disabled()) return;
225
+
226
+ const droppedFiles = event.dataTransfer?.files;
227
+ if (droppedFiles && droppedFiles.length > 0) {
228
+ let newFiles = Array.from(droppedFiles);
229
+
230
+ // Filter by accept if specified
231
+ const acceptAttr = this.accept();
232
+ if (acceptAttr) {
233
+ newFiles = this.filterFilesByAccept(newFiles, acceptAttr);
234
+ }
235
+
236
+ // Handle multiple vs single
237
+ if (!this.multiple() && newFiles.length > 1) {
238
+ newFiles = [newFiles[0]];
239
+ }
240
+
241
+ this.files.set(newFiles);
242
+ }
243
+ }
244
+
245
+ /** Remove a specific file from the selection. */
246
+ removeFile(fileToRemove: File): void {
247
+ if (this.disabled()) return;
248
+ const updatedFiles = this.files().filter(f => f !== fileToRemove);
249
+ this.files.set(updatedFiles);
250
+
251
+ // Clear the native input
252
+ const inputEl = this.fileInputRef()?.nativeElement;
253
+ if (inputEl) {
254
+ inputEl.value = '';
255
+ }
256
+ }
257
+
258
+ /** Clear all selected files. */
259
+ clearFiles(): void {
260
+ this.files.set([]);
261
+ const inputEl = this.fileInputRef()?.nativeElement;
262
+ if (inputEl) {
263
+ inputEl.value = '';
264
+ }
265
+ }
266
+
267
+ private filterFilesByAccept(files: File[], accept: string): File[] {
268
+ const acceptTypes = accept.split(',').map(t => t.trim().toLowerCase());
269
+ return files.filter(file => {
270
+ const fileName = file.name.toLowerCase();
271
+ const fileType = file.type.toLowerCase();
272
+
273
+ return acceptTypes.some(acceptType => {
274
+ if (acceptType.startsWith('.')) {
275
+ // Extension match (e.g., .jpg, .png)
276
+ return fileName.endsWith(acceptType);
277
+ } else if (acceptType.endsWith('/*')) {
278
+ // MIME type wildcard (e.g., image/*)
279
+ const category = acceptType.slice(0, -2);
280
+ return fileType.startsWith(category);
281
+ } else {
282
+ // Exact MIME type match
283
+ return fileType === acceptType;
284
+ }
285
+ });
286
+ });
287
+ }
288
+ }
@@ -0,0 +1,6 @@
1
+ import { clsx, type ClassValue } from 'clsx';
2
+ import { twMerge } from 'tailwind-merge';
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs));
6
+ }
@@ -0,0 +1,3 @@
1
+ export * from './sng-hover-card';
2
+ export * from './sng-hover-card-trigger';
3
+ export * from './sng-hover-card-content';
@@ -0,0 +1,100 @@
1
+ import {
2
+ Component,
3
+ ChangeDetectionStrategy,
4
+ ViewEncapsulation,
5
+ TemplateRef,
6
+ ViewContainerRef,
7
+ inject,
8
+ viewChild,
9
+ input,
10
+ computed,
11
+ } from '@angular/core';
12
+ import type { SngHoverCard } from './sng-hover-card';
13
+ import { cn } from './cn';
14
+
15
+ /**
16
+ * Component that renders the hover card content panel.
17
+ * Must be used inside a SngHoverCard container.
18
+ *
19
+ * @example
20
+ * ```html
21
+ * <sng-hover-card>
22
+ * <sng-hover-card-trigger href="#">@user</sng-hover-card-trigger>
23
+ * <sng-hover-card-content class="w-80">
24
+ * <div class="flex gap-4">
25
+ * <img src="avatar.jpg" alt="Avatar" />
26
+ * <div>
27
+ * <h4>User Name</h4>
28
+ * <p>User bio here</p>
29
+ * </div>
30
+ * </div>
31
+ * </sng-hover-card-content>
32
+ * </sng-hover-card>
33
+ * ```
34
+ */
35
+ @Component({
36
+ selector: 'sng-hover-card-content',
37
+ standalone: true,
38
+ changeDetection: ChangeDetectionStrategy.OnPush,
39
+ encapsulation: ViewEncapsulation.None,
40
+ styles: [`
41
+ .sng-hover-card-content[data-state=open][data-side=bottom] { animation: sng-hover-card-enter-bottom 150ms ease both; }
42
+ .sng-hover-card-content[data-state=open][data-side=top] { animation: sng-hover-card-enter-top 150ms ease both; }
43
+ .sng-hover-card-content[data-state=open][data-side=left] { animation: sng-hover-card-enter-left 150ms ease both; }
44
+ .sng-hover-card-content[data-state=open][data-side=right] { animation: sng-hover-card-enter-right 150ms ease both; }
45
+ .sng-hover-card-content[data-state=closed] { animation: sng-hover-card-exit 150ms ease both; }
46
+ @keyframes sng-hover-card-enter-bottom { from { opacity: 0; transform: scale(0.95) translateY(-0.5rem); } }
47
+ @keyframes sng-hover-card-enter-top { from { opacity: 0; transform: scale(0.95) translateY(0.5rem); } }
48
+ @keyframes sng-hover-card-enter-left { from { opacity: 0; transform: scale(0.95) translateX(0.5rem); } }
49
+ @keyframes sng-hover-card-enter-right { from { opacity: 0; transform: scale(0.95) translateX(-0.5rem); } }
50
+ @keyframes sng-hover-card-exit { to { opacity: 0; transform: scale(0.95); } }
51
+ `],
52
+ host: {},
53
+ template: `
54
+ <ng-template #content>
55
+ <div
56
+ class="sng-hover-card-content"
57
+ [class]="hostClasses()"
58
+ data-state="open"
59
+ [attr.data-side]="dataSide()"
60
+ (mouseenter)="onMouseEnter()"
61
+ (mouseleave)="onMouseLeave()"
62
+ >
63
+ <ng-content />
64
+ </div>
65
+ </ng-template>
66
+ `,
67
+ })
68
+ export class SngHoverCardContent {
69
+ /**
70
+ * Custom CSS classes for the hover card content panel.
71
+ */
72
+ class = input<string>('');
73
+
74
+ // Assigned by parent
75
+ hoverCard: SngHoverCard | null = null;
76
+
77
+ private contentTemplate = viewChild.required<TemplateRef<unknown>>('content');
78
+ viewContainerRef = inject(ViewContainerRef);
79
+
80
+ dataSide = computed(() => this.hoverCard?.side() ?? 'bottom');
81
+
82
+ hostClasses = computed(() =>
83
+ cn(
84
+ 'z-50 w-64 rounded-md border border-border bg-popover text-popover-foreground p-4 shadow-md outline-none',
85
+ this.class()
86
+ )
87
+ );
88
+
89
+ get templateRef(): TemplateRef<unknown> {
90
+ return this.contentTemplate();
91
+ }
92
+
93
+ onMouseEnter() {
94
+ this.hoverCard?.onContentEnter();
95
+ }
96
+
97
+ onMouseLeave() {
98
+ this.hoverCard?.onContentLeave();
99
+ }
100
+ }
@@ -0,0 +1,43 @@
1
+ import {
2
+ Directive,
3
+ ElementRef,
4
+ inject,
5
+ } from '@angular/core';
6
+ import type { SngHoverCard } from './sng-hover-card';
7
+
8
+ /**
9
+ * Directive that marks an element as the trigger for a hover card.
10
+ * Opens the hover card on mouse enter and closes on mouse leave.
11
+ *
12
+ * @example
13
+ * ```html
14
+ * <sng-hover-card>
15
+ * <sng-hover-card-trigger href="#">Hover over me</sng-hover-card-trigger>
16
+ * <sng-hover-card-content>Content</sng-hover-card-content>
17
+ * </sng-hover-card>
18
+ * ```
19
+ */
20
+ @Directive({
21
+ selector: 'sng-hover-card-trigger',
22
+ standalone: true,
23
+ host: {
24
+ '(mouseenter)': 'onMouseEnter()',
25
+ '(mouseleave)': 'onMouseLeave()',
26
+ },
27
+ })
28
+ export class SngHoverCardTrigger {
29
+ private elementRef = inject(ElementRef);
30
+ private hoverCard: SngHoverCard | null = null;
31
+
32
+ registerHoverCard(hoverCard: SngHoverCard) {
33
+ this.hoverCard = hoverCard;
34
+ }
35
+
36
+ onMouseEnter() {
37
+ this.hoverCard?.onTriggerEnter(this.elementRef);
38
+ }
39
+
40
+ onMouseLeave() {
41
+ this.hoverCard?.onTriggerLeave();
42
+ }
43
+ }
@@ -0,0 +1,246 @@
1
+ import {
2
+ Component,
3
+ ChangeDetectionStrategy,
4
+ ViewEncapsulation,
5
+ input,
6
+ computed,
7
+ contentChild,
8
+ ElementRef,
9
+ inject,
10
+ OnDestroy,
11
+ AfterContentInit,
12
+ signal,
13
+ WritableSignal,
14
+ } from '@angular/core';
15
+ import { Overlay, OverlayPositionBuilder, OverlayRef, ConnectedPosition } from '@angular/cdk/overlay';
16
+ import { TemplatePortal } from '@angular/cdk/portal';
17
+ import { Subscription, timer } from 'rxjs';
18
+ import { SngHoverCardTrigger } from './sng-hover-card-trigger';
19
+ import { SngHoverCardContent } from './sng-hover-card-content';
20
+ import { cn } from './cn';
21
+
22
+ type OverlaySide = 'top' | 'bottom' | 'left' | 'right';
23
+
24
+ function getOverlayPosition(side: OverlaySide, offset = 4): ConnectedPosition {
25
+ const positions: Record<OverlaySide, ConnectedPosition> = {
26
+ top: { originX: 'center', originY: 'top', overlayX: 'center', overlayY: 'bottom', offsetY: -offset },
27
+ bottom: { originX: 'center', originY: 'bottom', overlayX: 'center', overlayY: 'top', offsetY: offset },
28
+ left: { originX: 'start', originY: 'center', overlayX: 'end', overlayY: 'center', offsetX: -offset },
29
+ right: { originX: 'end', originY: 'center', overlayX: 'start', overlayY: 'center', offsetX: offset },
30
+ };
31
+ return positions[side];
32
+ }
33
+
34
+ class OverlayLifecycle {
35
+ private _overlayRef: OverlayRef | null = null;
36
+ private _subscriptions = new Subscription();
37
+ private _closing = false;
38
+ readonly isOpen: WritableSignal<boolean> = signal(false);
39
+
40
+ attach(overlayRef: OverlayRef): void {
41
+ this._overlayRef = overlayRef;
42
+ this.isOpen.set(true);
43
+ }
44
+
45
+ addSubscription(subscription: Subscription): void { this._subscriptions.add(subscription); }
46
+ hasOverlay(): boolean { return this._overlayRef !== null; }
47
+
48
+ /**
49
+ * Closes the overlay with a CSS exit animation, then disposes.
50
+ * Sets data-state="closed" on overlay elements and waits for all CSS
51
+ * animations to finish (via Web Animations API) before removing the overlay.
52
+ */
53
+ close(): void {
54
+ if (!this._overlayRef || this._closing) return;
55
+ this._closing = true;
56
+ this.isOpen.set(false);
57
+
58
+ const panel = this._overlayRef.overlayElement;
59
+ panel.querySelectorAll('[data-state]').forEach(el =>
60
+ el.setAttribute('data-state', 'closed')
61
+ );
62
+
63
+ const animations = panel.getAnimations({ subtree: true });
64
+ if (animations.length > 0) {
65
+ Promise.allSettled(animations.map(animation => animation.finished)).finally(() => this.dispose());
66
+ } else {
67
+ this.dispose();
68
+ }
69
+ }
70
+
71
+ dispose(): void {
72
+ this._closing = false;
73
+ this._subscriptions.unsubscribe();
74
+ this._subscriptions = new Subscription();
75
+ if (this._overlayRef) { this._overlayRef.detach(); this._overlayRef.dispose(); this._overlayRef = null; }
76
+ this.isOpen.set(false);
77
+ }
78
+
79
+ destroy(): void { this.dispose(); }
80
+ }
81
+
82
+ export type HoverCardSide = OverlaySide;
83
+
84
+ /**
85
+ * Container component for hover card functionality.
86
+ * Displays a content panel on hover with configurable delays.
87
+ * Uses CDK Overlay for positioning.
88
+ *
89
+ * @example
90
+ * ```html
91
+ * <sng-hover-card [openDelay]="300" [closeDelay]="200">
92
+ * <sng-hover-card-trigger href="#">@username</sng-hover-card-trigger>
93
+ * <sng-hover-card-content>
94
+ * <p>User profile preview</p>
95
+ * </sng-hover-card-content>
96
+ * </sng-hover-card>
97
+ * ```
98
+ */
99
+ @Component({
100
+ selector: 'sng-hover-card',
101
+ standalone: true,
102
+ changeDetection: ChangeDetectionStrategy.OnPush,
103
+ encapsulation: ViewEncapsulation.None,
104
+ host: {
105
+ '[class]': 'hostClasses()',
106
+ },
107
+ template: `<ng-content />`,
108
+ })
109
+ export class SngHoverCard implements AfterContentInit, OnDestroy {
110
+ /**
111
+ * Custom CSS classes for the hover card container.
112
+ */
113
+ class = input<string>('');
114
+
115
+ /**
116
+ * Position of the hover card relative to the trigger element.
117
+ */
118
+ side = input<HoverCardSide>('bottom');
119
+
120
+ /**
121
+ * Delay in milliseconds before opening the hover card.
122
+ */
123
+ openDelay = input<number>(200);
124
+
125
+ /**
126
+ * Delay in milliseconds before closing the hover card.
127
+ */
128
+ closeDelay = input<number>(300);
129
+
130
+ private overlay = inject(Overlay);
131
+ private overlayPositionBuilder = inject(OverlayPositionBuilder);
132
+ private lifecycle = new OverlayLifecycle();
133
+ private openTimerSubscription: Subscription | null = null;
134
+ private closeTimerSubscription: Subscription | null = null;
135
+
136
+ trigger = contentChild(SngHoverCardTrigger);
137
+ content = contentChild(SngHoverCardContent);
138
+
139
+ isOpen = this.lifecycle.isOpen;
140
+
141
+ hostClasses = computed(() => cn('inline-block', this.class()));
142
+
143
+ ngAfterContentInit() {
144
+ const triggerDirective = this.trigger();
145
+ if (triggerDirective) {
146
+ triggerDirective.registerHoverCard(this);
147
+ }
148
+ }
149
+
150
+ onTriggerEnter(triggerElement: ElementRef) {
151
+ this.clearCloseTimeout();
152
+ if (this.isOpen()) return;
153
+ const delay = Math.max(0, this.openDelay());
154
+ if (delay === 0) {
155
+ this.show(triggerElement);
156
+ return;
157
+ }
158
+ this.openTimerSubscription = timer(delay).subscribe(() => {
159
+ this.openTimerSubscription = null;
160
+ this.show(triggerElement);
161
+ });
162
+ }
163
+
164
+ onTriggerLeave() {
165
+ this.clearOpenTimeout();
166
+ this.scheduleClose();
167
+ }
168
+
169
+ onContentEnter() {
170
+ this.clearCloseTimeout();
171
+ }
172
+
173
+ onContentLeave() {
174
+ this.scheduleClose();
175
+ }
176
+
177
+ private scheduleClose() {
178
+ const delay = Math.max(0, this.closeDelay());
179
+ if (delay === 0) {
180
+ this.hide();
181
+ return;
182
+ }
183
+ this.closeTimerSubscription = timer(delay).subscribe(() => {
184
+ this.closeTimerSubscription = null;
185
+ this.hide();
186
+ });
187
+ }
188
+
189
+ private clearOpenTimeout() {
190
+ this.openTimerSubscription?.unsubscribe();
191
+ this.openTimerSubscription = null;
192
+ }
193
+
194
+ private clearCloseTimeout() {
195
+ this.closeTimerSubscription?.unsubscribe();
196
+ this.closeTimerSubscription = null;
197
+ }
198
+
199
+ private show(triggerElement: ElementRef) {
200
+ if (this.lifecycle.hasOverlay()) return;
201
+
202
+ const primary = this.side();
203
+ const fallbackMap: Record<OverlaySide, OverlaySide> = {
204
+ bottom: 'top', top: 'bottom', left: 'right', right: 'left',
205
+ };
206
+ const positionStrategy = this.overlayPositionBuilder
207
+ .flexibleConnectedTo(triggerElement)
208
+ .withPositions([
209
+ getOverlayPosition(primary),
210
+ getOverlayPosition(fallbackMap[primary]),
211
+ ])
212
+ .withPush(true)
213
+ .withViewportMargin(8);
214
+
215
+ const overlayRef = this.overlay.create({
216
+ positionStrategy,
217
+ scrollStrategy: this.overlay.scrollStrategies.close(),
218
+ });
219
+
220
+ const contentComponent = this.content();
221
+ if (contentComponent) {
222
+ contentComponent.hoverCard = this;
223
+ const portal = new TemplatePortal(
224
+ contentComponent.templateRef,
225
+ contentComponent.viewContainerRef
226
+ );
227
+ overlayRef.attach(portal);
228
+ }
229
+
230
+ this.lifecycle.attach(overlayRef);
231
+ this.lifecycle.addSubscription(
232
+ overlayRef.detachments().subscribe(() => this.lifecycle.dispose())
233
+ );
234
+ }
235
+
236
+ hide() {
237
+ if (!this.lifecycle.hasOverlay()) return;
238
+ this.lifecycle.close();
239
+ }
240
+
241
+ ngOnDestroy() {
242
+ this.clearOpenTimeout();
243
+ this.clearCloseTimeout();
244
+ this.lifecycle.destroy();
245
+ }
246
+ }
@@ -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 @@
1
+ export { SngInput, type SngInputType } from './sng-input';