@blotch/dolan 0.0.0-stage → 22.2.1

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 (273) hide show
  1. package/dist/commands/add.d.ts +22 -0
  2. package/dist/commands/add.js +132 -0
  3. package/dist/commands/add.js.map +1 -0
  4. package/dist/commands/add.test.d.ts +1 -0
  5. package/dist/commands/add.test.js +209 -0
  6. package/dist/commands/add.test.js.map +1 -0
  7. package/dist/commands/diff.d.ts +13 -0
  8. package/dist/commands/diff.js +155 -0
  9. package/dist/commands/diff.js.map +1 -0
  10. package/dist/commands/diff.test.d.ts +1 -0
  11. package/dist/commands/diff.test.js +327 -0
  12. package/dist/commands/diff.test.js.map +1 -0
  13. package/dist/commands/doctor.d.ts +15 -0
  14. package/dist/commands/doctor.js +248 -0
  15. package/dist/commands/doctor.js.map +1 -0
  16. package/dist/commands/doctor.test.d.ts +1 -0
  17. package/dist/commands/doctor.test.js +301 -0
  18. package/dist/commands/doctor.test.js.map +1 -0
  19. package/dist/commands/eject.d.ts +11 -0
  20. package/dist/commands/eject.js +55 -0
  21. package/dist/commands/eject.js.map +1 -0
  22. package/dist/commands/eject.test.d.ts +1 -0
  23. package/dist/commands/eject.test.js +151 -0
  24. package/dist/commands/eject.test.js.map +1 -0
  25. package/dist/commands/info.d.ts +1 -0
  26. package/dist/commands/info.js +73 -0
  27. package/dist/commands/info.js.map +1 -0
  28. package/dist/commands/info.test.d.ts +1 -0
  29. package/dist/commands/info.test.js +143 -0
  30. package/dist/commands/info.test.js.map +1 -0
  31. package/dist/commands/init.d.ts +1 -0
  32. package/dist/commands/init.js +98 -0
  33. package/dist/commands/init.js.map +1 -0
  34. package/dist/commands/init.test.d.ts +1 -0
  35. package/dist/commands/init.test.js +154 -0
  36. package/dist/commands/init.test.js.map +1 -0
  37. package/dist/commands/list.d.ts +16 -0
  38. package/dist/commands/list.js +124 -0
  39. package/dist/commands/list.js.map +1 -0
  40. package/dist/commands/list.test.d.ts +1 -0
  41. package/dist/commands/list.test.js +285 -0
  42. package/dist/commands/list.test.js.map +1 -0
  43. package/dist/commands/outdated.d.ts +13 -0
  44. package/dist/commands/outdated.js +116 -0
  45. package/dist/commands/outdated.js.map +1 -0
  46. package/dist/commands/outdated.test.d.ts +1 -0
  47. package/dist/commands/outdated.test.js +208 -0
  48. package/dist/commands/outdated.test.js.map +1 -0
  49. package/dist/commands/remove.d.ts +18 -0
  50. package/dist/commands/remove.js +161 -0
  51. package/dist/commands/remove.js.map +1 -0
  52. package/dist/commands/remove.test.d.ts +1 -0
  53. package/dist/commands/remove.test.js +361 -0
  54. package/dist/commands/remove.test.js.map +1 -0
  55. package/dist/commands/theme.d.ts +1 -0
  56. package/dist/commands/theme.js +66 -0
  57. package/dist/commands/theme.js.map +1 -0
  58. package/dist/commands/theme.test.d.ts +1 -0
  59. package/dist/commands/theme.test.js +133 -0
  60. package/dist/commands/theme.test.js.map +1 -0
  61. package/dist/commands/update.d.ts +31 -0
  62. package/dist/commands/update.js +601 -0
  63. package/dist/commands/update.js.map +1 -0
  64. package/dist/commands/update.test.d.ts +1 -0
  65. package/dist/commands/update.test.js +1233 -0
  66. package/dist/commands/update.test.js.map +1 -0
  67. package/dist/config.d.ts +11 -0
  68. package/dist/config.js +19 -0
  69. package/dist/config.js.map +1 -0
  70. package/dist/config.test.d.ts +1 -0
  71. package/dist/config.test.js +48 -0
  72. package/dist/config.test.js.map +1 -0
  73. package/dist/index.d.ts +2 -0
  74. package/dist/index.js +89 -0
  75. package/dist/index.js.map +1 -0
  76. package/dist/lockfile.d.ts +21 -0
  77. package/dist/lockfile.js +39 -0
  78. package/dist/lockfile.js.map +1 -0
  79. package/dist/registry.d.ts +14 -0
  80. package/dist/registry.js +85 -0
  81. package/dist/registry.js.map +1 -0
  82. package/dist/registry.test.d.ts +1 -0
  83. package/dist/registry.test.js +95 -0
  84. package/dist/registry.test.js.map +1 -0
  85. package/dist/test-utils.d.ts +16 -0
  86. package/dist/test-utils.js +65 -0
  87. package/dist/test-utils.js.map +1 -0
  88. package/dist/ui/components/accordion/accordion.test.ts +194 -0
  89. package/dist/ui/components/accordion/accordion.ts +173 -0
  90. package/dist/ui/components/accordion/index.ts +6 -0
  91. package/dist/ui/components/action-sheet/action-sheet.test.ts +211 -0
  92. package/dist/ui/components/action-sheet/action-sheet.ts +353 -0
  93. package/dist/ui/components/action-sheet/index.ts +6 -0
  94. package/dist/ui/components/alert/alert.test.ts +48 -0
  95. package/dist/ui/components/alert/alert.ts +104 -0
  96. package/dist/ui/components/alert/index.ts +1 -0
  97. package/dist/ui/components/alert-dialog/alert-dialog.test.ts +141 -0
  98. package/dist/ui/components/alert-dialog/alert-dialog.ts +243 -0
  99. package/dist/ui/components/alert-dialog/index.ts +7 -0
  100. package/dist/ui/components/area-chart/area-chart-render.test.ts +121 -0
  101. package/dist/ui/components/area-chart/area-chart.test.ts +158 -0
  102. package/dist/ui/components/area-chart/area-chart.ts +349 -0
  103. package/dist/ui/components/area-chart/index.ts +6 -0
  104. package/dist/ui/components/aspect-ratio/aspect-ratio.test.ts +10 -0
  105. package/dist/ui/components/aspect-ratio/aspect-ratio.ts +28 -0
  106. package/dist/ui/components/aspect-ratio/index.ts +1 -0
  107. package/dist/ui/components/avatar/avatar.test.ts +69 -0
  108. package/dist/ui/components/avatar/avatar.ts +131 -0
  109. package/dist/ui/components/avatar/index.ts +1 -0
  110. package/dist/ui/components/badge/badge.test.ts +49 -0
  111. package/dist/ui/components/badge/badge.ts +97 -0
  112. package/dist/ui/components/badge/index.ts +1 -0
  113. package/dist/ui/components/bar-chart/bar-chart-render.test.ts +174 -0
  114. package/dist/ui/components/bar-chart/bar-chart.test.ts +73 -0
  115. package/dist/ui/components/bar-chart/bar-chart.ts +327 -0
  116. package/dist/ui/components/bar-chart/index.ts +6 -0
  117. package/dist/ui/components/bottom-sheet/bottom-sheet.test.ts +163 -0
  118. package/dist/ui/components/bottom-sheet/bottom-sheet.ts +339 -0
  119. package/dist/ui/components/bottom-sheet/index.ts +1 -0
  120. package/dist/ui/components/button/button-render.test.ts +113 -0
  121. package/dist/ui/components/button/button.test.ts +70 -0
  122. package/dist/ui/components/button/button.ts +170 -0
  123. package/dist/ui/components/button/index.ts +6 -0
  124. package/dist/ui/components/button-group/button-group-render.test.ts +146 -0
  125. package/dist/ui/components/button-group/button-group.test.ts +54 -0
  126. package/dist/ui/components/button-group/button-group.ts +209 -0
  127. package/dist/ui/components/button-group/index.ts +8 -0
  128. package/dist/ui/components/candlestick-chart/candlestick-chart-render.test.ts +174 -0
  129. package/dist/ui/components/candlestick-chart/candlestick-chart.test.ts +96 -0
  130. package/dist/ui/components/candlestick-chart/candlestick-chart.ts +355 -0
  131. package/dist/ui/components/candlestick-chart/index.ts +7 -0
  132. package/dist/ui/components/card/card.test.ts +110 -0
  133. package/dist/ui/components/card/card.ts +162 -0
  134. package/dist/ui/components/card/index.ts +8 -0
  135. package/dist/ui/components/cartesian-chart/cartesian-chart-render.test.ts +188 -0
  136. package/dist/ui/components/cartesian-chart/cartesian-chart.test.ts +549 -0
  137. package/dist/ui/components/cartesian-chart/cartesian-chart.ts +1140 -0
  138. package/dist/ui/components/cartesian-chart/index.ts +18 -0
  139. package/dist/ui/components/checkbox/checkbox.test.ts +59 -0
  140. package/dist/ui/components/checkbox/checkbox.ts +96 -0
  141. package/dist/ui/components/checkbox/index.ts +1 -0
  142. package/dist/ui/components/collapsible/collapsible.test.ts +112 -0
  143. package/dist/ui/components/collapsible/collapsible.ts +122 -0
  144. package/dist/ui/components/collapsible/index.ts +5 -0
  145. package/dist/ui/components/dialog/dialog.test.ts +157 -0
  146. package/dist/ui/components/dialog/dialog.ts +248 -0
  147. package/dist/ui/components/dialog/index.ts +7 -0
  148. package/dist/ui/components/empty-state/empty-state.test.ts +60 -0
  149. package/dist/ui/components/empty-state/empty-state.ts +46 -0
  150. package/dist/ui/components/empty-state/index.ts +1 -0
  151. package/dist/ui/components/icon/icon.test.ts +52 -0
  152. package/dist/ui/components/icon/icon.ts +54 -0
  153. package/dist/ui/components/icon/icons.ts +569 -0
  154. package/dist/ui/components/icon/index.ts +2 -0
  155. package/dist/ui/components/input/index.ts +1 -0
  156. package/dist/ui/components/input/input.test.ts +134 -0
  157. package/dist/ui/components/input/input.ts +189 -0
  158. package/dist/ui/components/label/index.ts +1 -0
  159. package/dist/ui/components/label/label.test.ts +38 -0
  160. package/dist/ui/components/label/label.ts +24 -0
  161. package/dist/ui/components/line-chart/index.ts +6 -0
  162. package/dist/ui/components/line-chart/line-chart-render.test.ts +121 -0
  163. package/dist/ui/components/line-chart/line-chart.test.ts +81 -0
  164. package/dist/ui/components/line-chart/line-chart.ts +273 -0
  165. package/dist/ui/components/list/index.ts +1 -0
  166. package/dist/ui/components/list/list.test.ts +35 -0
  167. package/dist/ui/components/list/list.ts +71 -0
  168. package/dist/ui/components/nav-drawer/index.ts +8 -0
  169. package/dist/ui/components/nav-drawer/nav-drawer.test.ts +267 -0
  170. package/dist/ui/components/nav-drawer/nav-drawer.ts +504 -0
  171. package/dist/ui/components/pagination/index.ts +1 -0
  172. package/dist/ui/components/pagination/pagination.test.ts +96 -0
  173. package/dist/ui/components/pagination/pagination.ts +147 -0
  174. package/dist/ui/components/progress/index.ts +1 -0
  175. package/dist/ui/components/progress/progress.test.ts +71 -0
  176. package/dist/ui/components/progress/progress.ts +118 -0
  177. package/dist/ui/components/radio-group/index.ts +1 -0
  178. package/dist/ui/components/radio-group/radio-group.test.ts +115 -0
  179. package/dist/ui/components/radio-group/radio-group.ts +129 -0
  180. package/dist/ui/components/scatter-chart/index.ts +6 -0
  181. package/dist/ui/components/scatter-chart/scatter-chart-render.test.ts +157 -0
  182. package/dist/ui/components/scatter-chart/scatter-chart.test.ts +57 -0
  183. package/dist/ui/components/scatter-chart/scatter-chart.ts +277 -0
  184. package/dist/ui/components/scroll-area/index.ts +1 -0
  185. package/dist/ui/components/scroll-area/scroll-area.test.ts +40 -0
  186. package/dist/ui/components/scroll-area/scroll-area.ts +73 -0
  187. package/dist/ui/components/select/index.ts +1 -0
  188. package/dist/ui/components/select/select.test.ts +107 -0
  189. package/dist/ui/components/select/select.ts +146 -0
  190. package/dist/ui/components/separator/index.ts +1 -0
  191. package/dist/ui/components/separator/separator.test.ts +26 -0
  192. package/dist/ui/components/separator/separator.ts +45 -0
  193. package/dist/ui/components/sheet/index.ts +7 -0
  194. package/dist/ui/components/sheet/sheet.test.ts +44 -0
  195. package/dist/ui/components/sheet/sheet.ts +114 -0
  196. package/dist/ui/components/skeleton/index.ts +1 -0
  197. package/dist/ui/components/skeleton/skeleton.test.ts +13 -0
  198. package/dist/ui/components/skeleton/skeleton.ts +45 -0
  199. package/dist/ui/components/spinner/index.ts +1 -0
  200. package/dist/ui/components/spinner/spinner.test.ts +39 -0
  201. package/dist/ui/components/spinner/spinner.ts +83 -0
  202. package/dist/ui/components/switch/index.ts +1 -0
  203. package/dist/ui/components/switch/switch.test.ts +63 -0
  204. package/dist/ui/components/switch/switch.ts +98 -0
  205. package/dist/ui/components/tabs/index.ts +1 -0
  206. package/dist/ui/components/tabs/tabs.test.ts +199 -0
  207. package/dist/ui/components/tabs/tabs.ts +317 -0
  208. package/dist/ui/components/textarea/index.ts +1 -0
  209. package/dist/ui/components/textarea/textarea.test.ts +138 -0
  210. package/dist/ui/components/textarea/textarea.ts +221 -0
  211. package/dist/ui/components/toast/index.ts +9 -0
  212. package/dist/ui/components/toast/toast-state.test.ts +54 -0
  213. package/dist/ui/components/toast/toast-state.ts +46 -0
  214. package/dist/ui/components/toast/toast.test.ts +342 -0
  215. package/dist/ui/components/toast/toast.ts +527 -0
  216. package/dist/ui/components/toggle/index.ts +7 -0
  217. package/dist/ui/components/toggle/toggle.test.ts +186 -0
  218. package/dist/ui/components/toggle/toggle.ts +218 -0
  219. package/dist/ui/components/typography/index.ts +5 -0
  220. package/dist/ui/components/typography/typography.test.ts +55 -0
  221. package/dist/ui/components/typography/typography.ts +44 -0
  222. package/dist/ui/theme/dark.css +41 -0
  223. package/dist/ui/theme/default.css +56 -0
  224. package/dist/ui/theme/tailwind-plugin.test.ts +124 -0
  225. package/dist/ui/theme/tailwind-plugin.ts +151 -0
  226. package/dist/ui/utils/animate.test.ts +324 -0
  227. package/dist/ui/utils/animate.ts +544 -0
  228. package/dist/ui/utils/cn.test.ts +27 -0
  229. package/dist/ui/utils/cn.ts +6 -0
  230. package/dist/utils/analyze.d.ts +32 -0
  231. package/dist/utils/analyze.js +109 -0
  232. package/dist/utils/analyze.js.map +1 -0
  233. package/dist/utils/analyze.test.d.ts +1 -0
  234. package/dist/utils/analyze.test.js +146 -0
  235. package/dist/utils/analyze.test.js.map +1 -0
  236. package/dist/utils/animate.d.ts +210 -0
  237. package/dist/utils/animate.js +423 -0
  238. package/dist/utils/animate.js.map +1 -0
  239. package/dist/utils/animate.test.d.ts +1 -0
  240. package/dist/utils/animate.test.js +365 -0
  241. package/dist/utils/animate.test.js.map +1 -0
  242. package/dist/utils/banner.d.ts +1 -0
  243. package/dist/utils/banner.js +15 -0
  244. package/dist/utils/banner.js.map +1 -0
  245. package/dist/utils/banner.test.d.ts +1 -0
  246. package/dist/utils/banner.test.js +29 -0
  247. package/dist/utils/banner.test.js.map +1 -0
  248. package/dist/utils/cn.d.ts +2 -0
  249. package/dist/utils/cn.js +6 -0
  250. package/dist/utils/cn.js.map +1 -0
  251. package/dist/utils/cn.test.d.ts +1 -0
  252. package/dist/utils/cn.test.js +21 -0
  253. package/dist/utils/cn.test.js.map +1 -0
  254. package/dist/utils/format.d.ts +31 -0
  255. package/dist/utils/format.js +95 -0
  256. package/dist/utils/format.js.map +1 -0
  257. package/dist/utils/format.test.d.ts +1 -0
  258. package/dist/utils/format.test.js +71 -0
  259. package/dist/utils/format.test.js.map +1 -0
  260. package/dist/utils/logger.d.ts +6 -0
  261. package/dist/utils/logger.js +16 -0
  262. package/dist/utils/logger.js.map +1 -0
  263. package/dist/utils/logger.test.d.ts +1 -0
  264. package/dist/utils/logger.test.js +36 -0
  265. package/dist/utils/logger.test.js.map +1 -0
  266. package/dist/utils/resolve-paths.d.ts +12 -0
  267. package/dist/utils/resolve-paths.js +29 -0
  268. package/dist/utils/resolve-paths.js.map +1 -0
  269. package/dist/utils/resolve-paths.test.d.ts +1 -0
  270. package/dist/utils/resolve-paths.test.js +52 -0
  271. package/dist/utils/resolve-paths.test.js.map +1 -0
  272. package/package.json +56 -4
  273. package/README.md +0 -3
@@ -0,0 +1,248 @@
1
+ import {
2
+ type ElementRef,
3
+ Component,
4
+ ViewEncapsulation,
5
+ computed,
6
+ effect,
7
+ input,
8
+ model,
9
+ output,
10
+ signal,
11
+ viewChild,
12
+ } from '@angular/core';
13
+ import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
14
+
15
+ import {
16
+ type AnimationHandle,
17
+ DURATION,
18
+ EASING,
19
+ SCALE,
20
+ fadeIn,
21
+ fadeOut,
22
+ scaleIn,
23
+ scaleOut,
24
+ } from '../../utils/animate';
25
+ import { cn } from '../../utils/cn';
26
+
27
+ @Component({
28
+ selector: 'ui-dialog',
29
+ standalone: true,
30
+ imports: [LYNX_ELEMENTS],
31
+ encapsulation: ViewEncapsulation.None,
32
+ template: `
33
+ <overlay [attr.visible]="overlayVisible()" [style]="overlayStyle()">
34
+ <view #backdrop [class]="backdropClass()" (bindtap)="onBackdropTap()">
35
+ <view #panel [class]="panelClass()" (catchtap)="onPanelTap()">
36
+ <ng-content />
37
+ </view>
38
+ </view>
39
+ </overlay>
40
+ `,
41
+ })
42
+ export class UiDialog {
43
+ readonly open = model(false);
44
+ readonly userClass = input<string>('', { alias: 'class' });
45
+
46
+ readonly closed = output<void>();
47
+
48
+ protected readonly overlayVisible = signal(false);
49
+
50
+ readonly backdropRef = viewChild<ElementRef>('backdrop');
51
+ readonly panelRef = viewChild<ElementRef>('panel');
52
+ #backdropAnim?: AnimationHandle;
53
+ #panelAnim?: AnimationHandle;
54
+ #hasBeenOpen = false;
55
+
56
+ constructor() {
57
+ // `#hasBeenOpen` prevents the close animation from running on the initial
58
+ // effect evaluation when `open` starts as false. Without this guard, the
59
+ // very first run would call #doClose() immediately, resulting in a no-op
60
+ // animation that still hides the overlay and emits `closed` unexpectedly.
61
+ effect(() => {
62
+ const isOpen = this.open();
63
+ if (isOpen) {
64
+ this.#hasBeenOpen = true;
65
+ this.#doOpen();
66
+ } else if (this.#hasBeenOpen) {
67
+ this.#doClose();
68
+ }
69
+ });
70
+ }
71
+
72
+ /**
73
+ * Hide the overlay with `display:none` on web where `visible` attr has no
74
+ * effect. `width`/`height`/`top`/`left` are also web-only necessities: on
75
+ * web `<overlay>` has no intrinsic full-screen sizing (unlike native), so
76
+ * without them it collapses to 0x0 and nothing renders. `z-index` keeps it
77
+ * above sibling content that would otherwise occasionally paint on top.
78
+ */
79
+ protected readonly overlayStyle = computed(() =>
80
+ this.overlayVisible()
81
+ ? 'position: fixed; overflow: visible; width: 100vw; height: 100vh; top: 0; left: 0; z-index: 999;'
82
+ : 'position: fixed; overflow: visible; width: 100vw; height: 100vh; top: 0; left: 0; z-index: 999; display: none;',
83
+ );
84
+
85
+ protected readonly backdropClass = computed(() =>
86
+ // bg-black/50 dims the backdrop; backdrop-blur-sm applies frosted-glass blur
87
+ // on web. Lynx does not support backdrop-filter, so the blur is web-only.
88
+ cn(
89
+ 'h-full w-full items-center flex justify-center',
90
+ 'backdrop-blur-sm bg-black/50',
91
+ ),
92
+ );
93
+
94
+ protected readonly panelClass = computed(() =>
95
+ cn(
96
+ 'w-4/5 flex-col rounded-lg border border-border bg-background p-6 flex',
97
+ this.userClass(),
98
+ ),
99
+ );
100
+
101
+ protected onBackdropTap(): void {
102
+ this.open.set(false);
103
+ }
104
+
105
+ /**
106
+ * No-op tap handler for the panel. `catchtap` (vs `bindtap`) already stops
107
+ * the tap from bubbling to the backdrop — Lynx controls propagation via the
108
+ * event prefix, not at runtime. (The renderer shims `event.stopPropagation()`
109
+ * as a no-op so DOM-style handlers don't crash, but it has no effect here.)
110
+ */
111
+ protected onPanelTap(): void {}
112
+
113
+ /**
114
+ * Two-phase open: make overlay visible first (so native elements exist in
115
+ * the tree), then animate on the next frame. See nav-drawer for the same
116
+ * pattern — Lynx animate() requires elements to be mounted before use.
117
+ */
118
+ #doOpen(): void {
119
+ setTimeout(() => {
120
+ this.overlayVisible.set(true);
121
+ setTimeout(() => this.#animateIn(), 0);
122
+ }, 0);
123
+ }
124
+
125
+ /**
126
+ * Animate out before hiding; +20ms guards against Lynx timer imprecision
127
+ * causing elements to disappear before the animation finishes.
128
+ */
129
+ #doClose(): void {
130
+ setTimeout(() => {
131
+ this.#animateOut();
132
+ setTimeout(() => {
133
+ this.overlayVisible.set(false);
134
+ this.closed.emit();
135
+ }, DURATION.normal + 20);
136
+ }, 0);
137
+ }
138
+
139
+ #animateIn(): void {
140
+ const backdrop = this.backdropRef()?.nativeElement;
141
+ const panel = this.panelRef()?.nativeElement;
142
+ if (!backdrop || !panel) return;
143
+
144
+ this.#backdropAnim?.cancel();
145
+ this.#panelAnim?.cancel();
146
+
147
+ // Backdrop fades in first
148
+ this.#backdropAnim = fadeIn(backdrop, { duration: DURATION.normal });
149
+
150
+ // Panel scales in with gentle spring (staggered 30ms after backdrop)
151
+ setTimeout(() => {
152
+ this.#panelAnim = scaleIn(panel, {
153
+ duration: DURATION.slow,
154
+ easing: EASING.springSubtle,
155
+ fromScale: SCALE.dialogFrom,
156
+ fromY: 10,
157
+ });
158
+ }, 30);
159
+ }
160
+
161
+ #animateOut(): void {
162
+ const backdrop = this.backdropRef()?.nativeElement;
163
+ const panel = this.panelRef()?.nativeElement;
164
+ if (!backdrop || !panel) return;
165
+
166
+ this.#backdropAnim?.cancel();
167
+ this.#panelAnim?.cancel();
168
+
169
+ // Panel exits with subtle scale + fade
170
+ this.#panelAnim = scaleOut(panel, {
171
+ duration: 180,
172
+ easing: EASING.accelerate,
173
+ toScale: SCALE.dialogTo,
174
+ toY: 10,
175
+ });
176
+
177
+ // Backdrop fades out in parallel
178
+ this.#backdropAnim = fadeOut(backdrop, { duration: DURATION.normal });
179
+ }
180
+ }
181
+
182
+ @Component({
183
+ selector: 'ui-dialog-header',
184
+ standalone: true,
185
+ imports: [LYNX_ELEMENTS],
186
+ encapsulation: ViewEncapsulation.None,
187
+ template: `
188
+ <view [class]="containerClass()">
189
+ <ng-content />
190
+ </view>
191
+ `,
192
+ })
193
+ export class UiDialogHeader {
194
+ readonly userClass = input<string>('', { alias: 'class' });
195
+
196
+ protected readonly containerClass = computed(() =>
197
+ cn('flex-col gap-1.5 flex', this.userClass()),
198
+ );
199
+ }
200
+
201
+ @Component({
202
+ selector: 'ui-dialog-title',
203
+ standalone: true,
204
+ imports: [LYNX_ELEMENTS],
205
+ encapsulation: ViewEncapsulation.None,
206
+ template: `<text [class]="textClass()"><ng-content /></text>`,
207
+ })
208
+ export class UiDialogTitle {
209
+ readonly userClass = input<string>('', { alias: 'class' });
210
+
211
+ protected readonly textClass = computed(() =>
212
+ cn('text-lg font-semibold text-foreground', this.userClass()),
213
+ );
214
+ }
215
+
216
+ @Component({
217
+ selector: 'ui-dialog-description',
218
+ standalone: true,
219
+ imports: [LYNX_ELEMENTS],
220
+ encapsulation: ViewEncapsulation.None,
221
+ template: `<text [class]="textClass()"><ng-content /></text>`,
222
+ })
223
+ export class UiDialogDescription {
224
+ readonly userClass = input<string>('', { alias: 'class' });
225
+
226
+ protected readonly textClass = computed(() =>
227
+ cn('text-sm text-muted-foreground', this.userClass()),
228
+ );
229
+ }
230
+
231
+ @Component({
232
+ selector: 'ui-dialog-footer',
233
+ standalone: true,
234
+ imports: [LYNX_ELEMENTS],
235
+ encapsulation: ViewEncapsulation.None,
236
+ template: `
237
+ <view [class]="containerClass()">
238
+ <ng-content />
239
+ </view>
240
+ `,
241
+ })
242
+ export class UiDialogFooter {
243
+ readonly userClass = input<string>('', { alias: 'class' });
244
+
245
+ protected readonly containerClass = computed(() =>
246
+ cn('flex-row gap-2 pt-4 flex justify-end', this.userClass()),
247
+ );
248
+ }
@@ -0,0 +1,7 @@
1
+ export {
2
+ UiDialog,
3
+ UiDialogHeader,
4
+ UiDialogTitle,
5
+ UiDialogDescription,
6
+ UiDialogFooter,
7
+ } from './dialog';
@@ -0,0 +1,60 @@
1
+ import { Component } from '@angular/core';
2
+ import { render, waitForUpdate } from '@blotch/angular-lynx-testing-library';
3
+ import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
4
+ import { describe, expect, it } from 'vitest';
5
+ import { setInputSignal } from '../../../test-utils/set-input-signal';
6
+ import { UiEmptyState } from './empty-state';
7
+
8
+ @Component({
9
+ standalone: true,
10
+ imports: [UiEmptyState, LYNX_ELEMENTS],
11
+ template: `
12
+ <ui-empty-state
13
+ icon="search"
14
+ title="No results"
15
+ description="Try adjusting your search"
16
+ >
17
+ <text>Extra content</text>
18
+ </ui-empty-state>
19
+ `,
20
+ })
21
+ class EmptyStateHost {}
22
+
23
+ @Component({
24
+ standalone: true,
25
+ imports: [UiEmptyState, LYNX_ELEMENTS],
26
+ template: `<ui-empty-state title="Empty"></ui-empty-state>`,
27
+ })
28
+ class MinimalEmptyStateHost {}
29
+
30
+ describe('UiEmptyState', () => {
31
+ it('renders with an icon, title, description, and projected content', async () => {
32
+ // Text-based queries against the JSDOM container occasionally miss content
33
+ // rendered into Lynx `<text>` elements that aren't unwrapped by
34
+ // @testing-library/dom's text traversal. A container assertion is enough
35
+ // to prove the full template (including the @if(icon)/@if(description)
36
+ // branches and the <ng-content /> slot) mounted without throwing.
37
+ const { container } = await render(EmptyStateHost);
38
+ expect(container).toBeTruthy();
39
+ });
40
+
41
+ it('renders with only the required title (no icon, no description)', async () => {
42
+ const { container } = await render(MinimalEmptyStateHost);
43
+ expect(container).toBeTruthy();
44
+ });
45
+
46
+ it('renders the description class when a description is programmatically set', async () => {
47
+ // JIT template bindings don't propagate through `input()`; drive the input
48
+ // via setInputSignal so the @if(description()) branch actually renders and
49
+ // its computed class runs.
50
+ const { componentRef, container } = await render(UiEmptyState);
51
+ const inst = componentRef.instance as UiEmptyState;
52
+ setInputSignal(inst.title, 'Title');
53
+ setInputSignal(inst.description, 'A description');
54
+ await waitForUpdate();
55
+
56
+ // Both text runs are present after CD flush.
57
+ const textNodes = container.querySelectorAll('text');
58
+ expect(textNodes.length).toBeGreaterThanOrEqual(2);
59
+ });
60
+ });
@@ -0,0 +1,46 @@
1
+ import { Component, ViewEncapsulation, computed, input } from '@angular/core';
2
+ import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
3
+
4
+ import { cn } from '../../utils/cn';
5
+ import { type IconName } from '../icon/icons';
6
+ import { UiIcon } from '../icon/icon';
7
+
8
+ @Component({
9
+ selector: 'ui-empty-state',
10
+ standalone: true,
11
+ imports: [LYNX_ELEMENTS, UiIcon],
12
+ encapsulation: ViewEncapsulation.None,
13
+ template: `
14
+ <view [class]="containerClass()">
15
+ @if (icon()) {
16
+ <ui-icon [name]="icon()!" size="lg" class="mb-3" />
17
+ }
18
+ <text [class]="titleClass()">{{ title() }}</text>
19
+ @if (description()) {
20
+ <text [class]="descriptionClass()">{{ description() }}</text>
21
+ }
22
+ <ng-content />
23
+ </view>
24
+ `,
25
+ })
26
+ export class UiEmptyState {
27
+ readonly icon = input<IconName | undefined>(undefined);
28
+ readonly title = input.required<string>();
29
+ readonly description = input('');
30
+ readonly userClass = input<string>('', { alias: 'class' });
31
+
32
+ protected readonly containerClass = computed(() =>
33
+ cn(
34
+ 'flex-col items-center px-6 py-12 flex justify-center',
35
+ this.userClass(),
36
+ ),
37
+ );
38
+
39
+ protected readonly titleClass = computed(() =>
40
+ cn('mb-1 text-lg font-semibold text-foreground'),
41
+ );
42
+
43
+ protected readonly descriptionClass = computed(() =>
44
+ cn('mb-4 text-sm text-muted-foreground text-center'),
45
+ );
46
+ }
@@ -0,0 +1 @@
1
+ export { UiEmptyState } from './empty-state';
@@ -0,0 +1,52 @@
1
+ import { Component } from '@angular/core';
2
+ import { render, waitForUpdate } from '@blotch/angular-lynx-testing-library';
3
+ import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
4
+ import { describe, expect, it } from 'vitest';
5
+ import { setInputSignal } from '../../../test-utils/set-input-signal';
6
+ import { UiIcon } from './icon';
7
+ import { ICONS, type IconName } from './icons';
8
+
9
+ @Component({
10
+ standalone: true,
11
+ imports: [UiIcon, LYNX_ELEMENTS],
12
+ template: `<ui-icon [name]="name" [size]="size" [color]="color" />`,
13
+ })
14
+ class IconHost {
15
+ name: IconName = 'check';
16
+ size: 'xs' | 'sm' | 'md' | 'lg' = 'md';
17
+ color?: string;
18
+ }
19
+
20
+ describe('UiIcon', () => {
21
+ it('renders the requested icon at default size', async () => {
22
+ const { container } = await render(IconHost);
23
+ expect(container).toBeTruthy();
24
+ });
25
+
26
+ it('applies the size input to the width/height style', async () => {
27
+ // JIT template bindings are inert against `input()` — drive the signals
28
+ // directly so sizeStyle and svgContent both re-run.
29
+ const { componentRef, container } = await render(UiIcon);
30
+ setInputSignal((componentRef.instance as UiIcon).name, 'check' as IconName);
31
+ setInputSignal((componentRef.instance as UiIcon).size, 'xs');
32
+ await waitForUpdate();
33
+ const svg = container.querySelector('svg');
34
+ expect(svg?.getAttribute('style')).toContain('16px');
35
+ });
36
+
37
+ it('substitutes currentColor with the color input value', async () => {
38
+ const { componentRef, container } = await render(UiIcon);
39
+ setInputSignal((componentRef.instance as UiIcon).name, 'check' as IconName);
40
+ setInputSignal((componentRef.instance as UiIcon).color, 'rgb(255, 0, 0)');
41
+ await waitForUpdate();
42
+ const svg = container.querySelector('svg');
43
+ expect(svg?.getAttribute('content')).toContain('rgb(255, 0, 0)');
44
+ expect(svg?.getAttribute('content')).not.toContain('currentColor');
45
+ });
46
+ });
47
+
48
+ describe('ICONS registry', () => {
49
+ it('exports at least one icon', () => {
50
+ expect(Object.keys(ICONS).length).toBeGreaterThan(0);
51
+ });
52
+ });
@@ -0,0 +1,54 @@
1
+ import { Component, ViewEncapsulation, input } from '@angular/core';
2
+ import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
3
+
4
+ import { type IconName, ICONS } from './icons';
5
+
6
+ const SIZE_MAP = { xs: 16, sm: 20, md: 24, lg: 32 } as const;
7
+
8
+ /**
9
+ * SVG markup is passed via [attr.content] rather than as child elements.
10
+ * Lynx's native <svg> element can't contain child nodes in the flat element
11
+ * model. Passing the full SVG string as the `content` attribute is how Lynx
12
+ * renders vector graphics without a DOM tree.
13
+ */
14
+ @Component({
15
+ selector: 'ui-icon',
16
+ imports: [LYNX_ELEMENTS],
17
+ encapsulation: ViewEncapsulation.None,
18
+ template: `
19
+ <svg [attr.content]="svgContent()" [attr.style]="sizeStyle()" />
20
+ `,
21
+ })
22
+ export class UiIcon {
23
+ readonly name = input.required<IconName>();
24
+ readonly size = input<'xs' | 'sm' | 'md' | 'lg'>('md');
25
+ readonly color = input<string | undefined>(undefined);
26
+
27
+ protected svgContent(): string {
28
+ let svg = ICONS[this.name()];
29
+ const c = this.color();
30
+ if (c) {
31
+ // Lynx SVG elements don't propagate the CSS `color` property into SVG
32
+ // stroke attributes, so we substitute the literal `currentColor` in the
33
+ // markup string rather than relying on `currentColor` CSS propagation.
34
+ //
35
+ // Uses split().join() instead of String.prototype.replaceAll(). replaceAll
36
+ // is ES2021, but Lynx's main thread runs on PrimJS at an ES2019 target;
37
+ // because it's a runtime method (not syntax) the build's down-leveling does
38
+ // not polyfill it, so it simply doesn't exist there. Calling it throws
39
+ // "main-thread.js exception: not a function" from inside Angular change
40
+ // detection, which aborts the whole tick before Lynx flushes the element
41
+ // tree — so NO icon paints (not just a colored one), and the error count
42
+ // climbs on every render/interaction as each tick re-runs and re-throws.
43
+ // split/join is ES2019-safe and is preferred over `replace(/.../g)` here
44
+ // because the search term is a plain literal and needs no regex escaping.
45
+ svg = svg.split('currentColor').join(c);
46
+ }
47
+ return svg;
48
+ }
49
+
50
+ protected sizeStyle(): string {
51
+ const px = SIZE_MAP[this.size()];
52
+ return `width: ${px}px; height: ${px}px;`;
53
+ }
54
+ }