@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,353 @@
1
+ import {
2
+ type ElementRef,
3
+ Component,
4
+ ViewEncapsulation,
5
+ computed,
6
+ effect,
7
+ inject,
8
+ input,
9
+ model,
10
+ output,
11
+ signal,
12
+ viewChild,
13
+ } from '@angular/core';
14
+ import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
15
+
16
+ import {
17
+ type AnimationHandle,
18
+ DURATION,
19
+ EASING,
20
+ SCALE,
21
+ fadeIn,
22
+ fadeOut,
23
+ pressDown,
24
+ pressRelease,
25
+ slideIn,
26
+ slideOut,
27
+ } from '../../utils/animate';
28
+ import { cn } from '../../utils/cn';
29
+
30
+ @Component({
31
+ selector: 'ui-action-sheet',
32
+ standalone: true,
33
+ imports: [LYNX_ELEMENTS],
34
+ encapsulation: ViewEncapsulation.None,
35
+ template: `
36
+ <overlay [attr.visible]="overlayVisible()" [style]="overlayStyle()">
37
+ <!-- bg-black/50 dims the content behind the sheet. The fadeIn/fadeOut in
38
+ #animateIn/#animateOut already tween this element's opacity over
39
+ DURATION.normal, so the dim animates in lockstep with the panel slide —
40
+ matching the dialog/bottom-sheet backdrop. A named color with an opacity
41
+ modifier is required here: bg-black/50 compiles to rgb(0 0 0 / 0.5),
42
+ which Lynx accepts, whereas opacity modifiers on semantic tokens don't. -->
43
+ <view
44
+ #backdrop
45
+ class="h-full w-full bg-black/50"
46
+ (bindtap)="onBackdropTap()"
47
+ >
48
+ <view
49
+ #panel
50
+ [class]="panelClass()"
51
+ [style]="panelPositionStyle()"
52
+ (catchtap)="onPanelTap()"
53
+ >
54
+ <!-- The elevation shadow is an inline rgba() box-shadow rather than a
55
+ \`shadow-*\` Tailwind class: the Lynx tailwind preset doesn't wire
56
+ up the \`--tw-shadow\`/\`--tw-ring-*\` variables those utilities
57
+ depend on, so \`shadow-*\` renders nothing on Lynx. \`overflow-hidden\`
58
+ clips this tile's children to the rounded corners but not its own
59
+ box-shadow (painted outside the border box), so the shadow shows. -->
60
+ <view
61
+ class="flex-col rounded-lg border border-border bg-card flex overflow-hidden"
62
+ style="box-shadow: 0 2px 16px rgba(0, 0, 0, 0.15);"
63
+ >
64
+ <ng-content />
65
+ </view>
66
+ <!-- Cancel button is slotted separately so it renders outside the
67
+ rounded card — matching the iOS action sheet visual convention
68
+ where cancel is a distinct tile below the action group. -->
69
+ <ng-content select="ui-action-sheet-cancel" />
70
+ </view>
71
+ </view>
72
+ </overlay>
73
+ `,
74
+ })
75
+ export class UiActionSheet {
76
+ readonly open = model(false);
77
+ readonly userClass = input<string>('', { alias: 'class' });
78
+
79
+ readonly closed = output<void>();
80
+
81
+ protected readonly overlayVisible = signal(false);
82
+ /**
83
+ * `width`/`height`/`top`/`left` are web-only necessities: on web
84
+ * `<overlay>` has no intrinsic full-screen sizing (unlike native), so
85
+ * without them it collapses to 0x0 and nothing renders. `z-index` keeps it
86
+ * above sibling content that would otherwise occasionally paint on top.
87
+ */
88
+ protected readonly overlayStyle = computed(() =>
89
+ this.overlayVisible()
90
+ ? 'position: fixed; overflow: visible; width: 100vw; height: 100vh; top: 0; left: 0; z-index: 999;'
91
+ : 'position: fixed; overflow: visible; width: 100vw; height: 100vh; top: 0; left: 0; z-index: 999; display: none;',
92
+ );
93
+
94
+ readonly backdropRef = viewChild<ElementRef>('backdrop');
95
+ readonly panelRef = viewChild<ElementRef>('panel');
96
+ #backdropAnim?: AnimationHandle;
97
+ #panelAnim?: AnimationHandle;
98
+ #hasBeenOpen = false;
99
+
100
+ constructor() {
101
+ // `#hasBeenOpen` prevents the close animation from running on the initial
102
+ // effect evaluation when `open` starts as false. Without this guard, the
103
+ // first run would call #doClose() and emit `closed` before the action
104
+ // sheet has ever been opened.
105
+ effect(() => {
106
+ const isOpen = this.open();
107
+ if (isOpen) {
108
+ this.#hasBeenOpen = true;
109
+ this.#doOpen();
110
+ } else if (this.#hasBeenOpen) {
111
+ this.#doClose();
112
+ }
113
+ });
114
+ }
115
+
116
+ protected readonly panelClass = computed(() =>
117
+ cn('w-full flex-col gap-2 px-4 pb-6 flex', this.userClass()),
118
+ );
119
+
120
+ protected readonly panelPositionStyle = computed(
121
+ () => 'position: absolute; bottom: 0; left: 0; right: 0;',
122
+ );
123
+
124
+ protected onBackdropTap(): void {
125
+ this.open.set(false);
126
+ }
127
+
128
+ close(): void {
129
+ this.open.set(false);
130
+ }
131
+
132
+ /**
133
+ * No-op tap handler for the panel. `catchtap` (vs `bindtap`) already stops
134
+ * the tap from bubbling to the backdrop — Lynx controls propagation via the
135
+ * event prefix, not at runtime. (The renderer shims `event.stopPropagation()`
136
+ * as a no-op so DOM-style handlers don't crash, but it has no effect here.)
137
+ */
138
+ protected onPanelTap(): void {}
139
+
140
+ /**
141
+ * Two-phase open: make overlay visible first (so native elements exist in
142
+ * the tree), then animate on the next frame. See nav-drawer for the same
143
+ * pattern — Lynx animate() requires elements to be mounted before use.
144
+ */
145
+ #doOpen(): void {
146
+ setTimeout(() => {
147
+ this.overlayVisible.set(true);
148
+ setTimeout(() => this.#animateIn(), 0);
149
+ }, 0);
150
+ }
151
+
152
+ /**
153
+ * Animate out before hiding; +20ms guards against Lynx timer imprecision
154
+ * causing elements to disappear before the animation finishes.
155
+ */
156
+ #doClose(): void {
157
+ setTimeout(() => {
158
+ this.#animateOut();
159
+ setTimeout(() => {
160
+ this.overlayVisible.set(false);
161
+ this.closed.emit();
162
+ }, DURATION.normal + 20);
163
+ }, 0);
164
+ }
165
+
166
+ #animateIn(): void {
167
+ const backdrop = this.backdropRef()?.nativeElement;
168
+ const panel = this.panelRef()?.nativeElement;
169
+ if (!backdrop || !panel) return;
170
+
171
+ this.#backdropAnim?.cancel();
172
+ this.#panelAnim?.cancel();
173
+
174
+ this.#backdropAnim = fadeIn(backdrop, { duration: DURATION.normal });
175
+ this.#panelAnim = slideIn(panel, 'up', {
176
+ duration: DURATION.slow,
177
+ easing: EASING.sheet,
178
+ });
179
+ }
180
+
181
+ #animateOut(): void {
182
+ const backdrop = this.backdropRef()?.nativeElement;
183
+ const panel = this.panelRef()?.nativeElement;
184
+ if (!backdrop || !panel) return;
185
+
186
+ this.#backdropAnim?.cancel();
187
+ this.#panelAnim?.cancel();
188
+
189
+ this.#backdropAnim = fadeOut(backdrop, { duration: DURATION.normal });
190
+ this.#panelAnim = slideOut(panel, 'down', {
191
+ duration: DURATION.normal,
192
+ easing: EASING.accelerate,
193
+ });
194
+ }
195
+ }
196
+
197
+ @Component({
198
+ selector: 'ui-action-sheet-title',
199
+ standalone: true,
200
+ imports: [LYNX_ELEMENTS],
201
+ encapsulation: ViewEncapsulation.None,
202
+ template: `
203
+ <view [class]="containerClass()">
204
+ <text [class]="textClass()"><ng-content /></text>
205
+ </view>
206
+ `,
207
+ })
208
+ export class UiActionSheetTitle {
209
+ readonly userClass = input<string>('', { alias: 'class' });
210
+
211
+ protected readonly containerClass = computed(() =>
212
+ cn('items-center px-4 py-3 flex justify-center', this.userClass()),
213
+ );
214
+
215
+ protected readonly textClass = computed(() =>
216
+ cn('text-sm text-muted-foreground'),
217
+ );
218
+ }
219
+
220
+ @Component({
221
+ selector: 'ui-action-sheet-item',
222
+ standalone: true,
223
+ imports: [LYNX_ELEMENTS],
224
+ encapsulation: ViewEncapsulation.None,
225
+ template: `
226
+ <view
227
+ #container
228
+ [class]="containerClass()"
229
+ (bindtouchstart)="onPressStart()"
230
+ (bindtouchend)="onPressEnd()"
231
+ (bindtouchcancel)="onPressCancel()"
232
+ (bindmousedown)="onPressStart()"
233
+ (bindmouseup)="onPressEnd()"
234
+ (bindmouseleave)="onPressCancel()"
235
+ (bindtap)="onTap()"
236
+ >
237
+ <text [class]="textClass()"><ng-content /></text>
238
+ </view>
239
+ `,
240
+ })
241
+ export class UiActionSheetItem {
242
+ readonly #sheet = inject(UiActionSheet);
243
+
244
+ readonly variant = input<'default' | 'destructive'>('default');
245
+ readonly userClass = input<string>('', { alias: 'class' });
246
+
247
+ readonly pressed = output<void>();
248
+
249
+ readonly containerRef = viewChild<ElementRef>('container');
250
+ #pressAnim?: AnimationHandle;
251
+
252
+ protected readonly containerClass = computed(() =>
253
+ cn(
254
+ 'items-center border-t border-border px-4 py-3 flex justify-center',
255
+ this.userClass(),
256
+ ),
257
+ );
258
+
259
+ protected readonly textClass = computed(() =>
260
+ cn(
261
+ 'text-base',
262
+ this.variant() === 'destructive' ? 'text-destructive' : 'text-primary',
263
+ ),
264
+ );
265
+
266
+ protected onPressStart(): void {
267
+ this.#pressAnim?.cancel();
268
+ this.#pressAnim = pressDown(
269
+ this.containerRef()?.nativeElement,
270
+ SCALE.pressDownLight,
271
+ );
272
+ }
273
+
274
+ protected onPressEnd(): void {
275
+ this.#pressAnim?.cancel();
276
+ this.#pressAnim = pressRelease(this.containerRef()?.nativeElement);
277
+ }
278
+
279
+ protected onPressCancel(): void {
280
+ this.#pressAnim?.cancel();
281
+ this.#pressAnim = pressRelease(this.containerRef()?.nativeElement);
282
+ }
283
+
284
+ protected onTap(): void {
285
+ this.pressed.emit();
286
+ this.#sheet.close();
287
+ }
288
+ }
289
+
290
+ @Component({
291
+ selector: 'ui-action-sheet-cancel',
292
+ standalone: true,
293
+ imports: [LYNX_ELEMENTS],
294
+ encapsulation: ViewEncapsulation.None,
295
+ template: `
296
+ <view
297
+ #container
298
+ [class]="containerClass()"
299
+ style="box-shadow: 0 2px 16px rgba(0, 0, 0, 0.15);"
300
+ (bindtouchstart)="onPressStart()"
301
+ (bindtouchend)="onPressEnd()"
302
+ (bindtouchcancel)="onPressCancel()"
303
+ (bindmousedown)="onPressStart()"
304
+ (bindmouseup)="onPressEnd()"
305
+ (bindmouseleave)="onPressCancel()"
306
+ (bindtap)="onTap()"
307
+ >
308
+ <text [class]="textClass()">{{ label() }}</text>
309
+ </view>
310
+ `,
311
+ })
312
+ export class UiActionSheetCancel {
313
+ readonly #sheet = inject(UiActionSheet);
314
+
315
+ readonly label = input('Cancel');
316
+ readonly userClass = input<string>('', { alias: 'class' });
317
+
318
+ readonly containerRef = viewChild<ElementRef>('container');
319
+ #pressAnim?: AnimationHandle;
320
+
321
+ protected readonly containerClass = computed(() =>
322
+ cn(
323
+ 'items-center rounded-lg border border-border bg-card px-4 py-3 flex justify-center',
324
+ this.userClass(),
325
+ ),
326
+ );
327
+
328
+ protected readonly textClass = computed(() =>
329
+ cn('text-base font-semibold text-primary'),
330
+ );
331
+
332
+ protected onPressStart(): void {
333
+ this.#pressAnim?.cancel();
334
+ this.#pressAnim = pressDown(
335
+ this.containerRef()?.nativeElement,
336
+ SCALE.pressDownLight,
337
+ );
338
+ }
339
+
340
+ protected onPressEnd(): void {
341
+ this.#pressAnim?.cancel();
342
+ this.#pressAnim = pressRelease(this.containerRef()?.nativeElement);
343
+ }
344
+
345
+ protected onPressCancel(): void {
346
+ this.#pressAnim?.cancel();
347
+ this.#pressAnim = pressRelease(this.containerRef()?.nativeElement);
348
+ }
349
+
350
+ protected onTap(): void {
351
+ this.#sheet.close();
352
+ }
353
+ }
@@ -0,0 +1,6 @@
1
+ export {
2
+ UiActionSheet,
3
+ UiActionSheetTitle,
4
+ UiActionSheetItem,
5
+ UiActionSheetCancel,
6
+ } from './action-sheet';
@@ -0,0 +1,48 @@
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 { UiAlert, alertVariants } from './alert';
7
+
8
+ @Component({
9
+ standalone: true,
10
+ imports: [UiAlert, LYNX_ELEMENTS],
11
+ template: `<ui-alert [variant]="variant"><text>Message</text></ui-alert>`,
12
+ })
13
+ class AlertHost {
14
+ variant: 'default' | 'destructive' = 'default';
15
+ }
16
+
17
+ describe('alertVariants', () => {
18
+ it('returns default variant classes', () => {
19
+ expect(alertVariants()).toContain('bg-background');
20
+ });
21
+
22
+ it('returns destructive variant classes', () => {
23
+ expect(alertVariants({ variant: 'destructive' })).toContain(
24
+ 'bg-destructive-subtle',
25
+ );
26
+ });
27
+ });
28
+
29
+ describe('UiAlert', () => {
30
+ it('renders projected content', async () => {
31
+ const { getByText } = await render(AlertHost);
32
+ expect(getByText('Message')).toBeTruthy();
33
+ });
34
+
35
+ it('renders title and description slots when their inputs are set', async () => {
36
+ // JIT template bindings don't propagate through input(); drive the signals
37
+ // directly so the @if(title()) / @if(description()) branches render and
38
+ // their computed class functions run.
39
+ const { componentRef, container } = await render(UiAlert);
40
+ const inst = componentRef.instance as UiAlert;
41
+ setInputSignal(inst.title, 'Heads up');
42
+ setInputSignal(inst.description, 'Your download is ready.');
43
+ await waitForUpdate();
44
+
45
+ const texts = container.querySelectorAll('text');
46
+ expect(texts.length).toBeGreaterThanOrEqual(2);
47
+ });
48
+ });
@@ -0,0 +1,104 @@
1
+ import {
2
+ type ElementRef,
3
+ Component,
4
+ ViewEncapsulation,
5
+ computed,
6
+ effect,
7
+ input,
8
+ viewChild,
9
+ } from '@angular/core';
10
+ import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
11
+ import { cva, type VariantProps } from 'class-variance-authority';
12
+
13
+ import { revealIn } from '../../utils/animate';
14
+ import { cn } from '../../utils/cn';
15
+
16
+ export const alertVariants = cva('flex-col rounded-lg border p-4 flex', {
17
+ variants: {
18
+ variant: {
19
+ default: 'border-border bg-background',
20
+ // Opacity modifiers on semantic colors don't work on Lynx (opaque rgba
21
+ // values with no separable channels): use a solid destructive border and
22
+ // the pre-composed translucent bg-destructive-subtle for the tint.
23
+ destructive: 'border-destructive bg-destructive-subtle',
24
+ },
25
+ },
26
+ defaultVariants: {
27
+ variant: 'default',
28
+ },
29
+ });
30
+
31
+ const alertTitleVariants = cva('mb-1 font-medium', {
32
+ variants: {
33
+ variant: {
34
+ default: 'text-foreground',
35
+ destructive: 'text-destructive',
36
+ },
37
+ },
38
+ defaultVariants: { variant: 'default' },
39
+ });
40
+
41
+ const alertDescriptionVariants = cva('text-sm', {
42
+ variants: {
43
+ variant: {
44
+ default: 'text-muted-foreground',
45
+ destructive: 'text-destructive',
46
+ },
47
+ },
48
+ defaultVariants: { variant: 'default' },
49
+ });
50
+
51
+ export type AlertVariant = NonNullable<
52
+ VariantProps<typeof alertVariants>['variant']
53
+ >;
54
+
55
+ @Component({
56
+ selector: 'ui-alert',
57
+ standalone: true,
58
+ imports: [LYNX_ELEMENTS],
59
+ encapsulation: ViewEncapsulation.None,
60
+ template: `
61
+ <view #container [class]="containerClass()">
62
+ @if (title()) {
63
+ <text [class]="titleClass()">{{ title() }}</text>
64
+ }
65
+ @if (description()) {
66
+ <text [class]="descriptionClass()">{{ description() }}</text>
67
+ }
68
+ <ng-content />
69
+ </view>
70
+ `,
71
+ })
72
+ export class UiAlert {
73
+ readonly variant = input<AlertVariant>('default');
74
+ readonly title = input<string>('');
75
+ readonly description = input<string>('');
76
+ readonly userClass = input<string>('', { alias: 'class' });
77
+
78
+ readonly containerRef = viewChild<ElementRef>('container');
79
+
80
+ constructor() {
81
+ // Subtle entrance animation on first render
82
+ effect(() => {
83
+ const el = this.containerRef()?.nativeElement;
84
+ // revealIn calls el.animate() which is inert in jsdom — the animation
85
+ // never plays and the effect can't be verified from a JIT render test.
86
+ /* v8 ignore next 3 */
87
+ if (el) {
88
+ revealIn(el, { fromY: 4 });
89
+ }
90
+ });
91
+ }
92
+
93
+ protected readonly containerClass = computed(() =>
94
+ cn(alertVariants({ variant: this.variant() }), this.userClass()),
95
+ );
96
+
97
+ protected readonly titleClass = computed(() =>
98
+ cn(alertTitleVariants({ variant: this.variant() })),
99
+ );
100
+
101
+ protected readonly descriptionClass = computed(() =>
102
+ cn(alertDescriptionVariants({ variant: this.variant() })),
103
+ );
104
+ }
@@ -0,0 +1 @@
1
+ export { UiAlert, alertVariants, type AlertVariant } from './alert';
@@ -0,0 +1,141 @@
1
+ import { Component, signal } 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 {
7
+ UiAlertDialog,
8
+ UiAlertDialogDescription,
9
+ UiAlertDialogFooter,
10
+ UiAlertDialogHeader,
11
+ UiAlertDialogTitle,
12
+ } from './alert-dialog';
13
+
14
+ @Component({
15
+ standalone: true,
16
+ imports: [
17
+ UiAlertDialog,
18
+ UiAlertDialogHeader,
19
+ UiAlertDialogTitle,
20
+ UiAlertDialogDescription,
21
+ UiAlertDialogFooter,
22
+ LYNX_ELEMENTS,
23
+ ],
24
+ template: `
25
+ <ui-alert-dialog [(open)]="open">
26
+ <ui-alert-dialog-header>
27
+ <ui-alert-dialog-title>Sure?</ui-alert-dialog-title>
28
+ <ui-alert-dialog-description>Cannot undo</ui-alert-dialog-description>
29
+ </ui-alert-dialog-header>
30
+ <ui-alert-dialog-footer><text>Buttons</text></ui-alert-dialog-footer>
31
+ </ui-alert-dialog>
32
+ `,
33
+ })
34
+ class AlertDialogHost {
35
+ open = true;
36
+ }
37
+
38
+ describe('UiAlertDialog', () => {
39
+ it('renders header, title, description, and footer', async () => {
40
+ const { getByText } = await render(AlertDialogHost);
41
+ expect(getByText('Sure?')).toBeTruthy();
42
+ expect(getByText('Cannot undo')).toBeTruthy();
43
+ expect(getByText('Buttons')).toBeTruthy();
44
+ });
45
+
46
+ it('panel tap is a no-op (catchtap stops bubbling)', async () => {
47
+ const { componentRef } = await render(UiAlertDialog);
48
+ const inst = componentRef.instance as UiAlertDialog;
49
+ expect(() =>
50
+ (inst as unknown as { onPanelTap: () => void }).onPanelTap(),
51
+ ).not.toThrow();
52
+ });
53
+
54
+ it('open effect: showing then hiding runs the animate-in/out lifecycles', async () => {
55
+ // Monkey-patch backdrop/panel refs so #animateIn / #animateOut execute in
56
+ // full instead of bailing on !backdrop || !panel.
57
+ const { componentRef, container } = await render(UiAlertDialog);
58
+ const inst = componentRef.instance as UiAlertDialog;
59
+ const view = container.querySelector('view') as Element;
60
+ (inst as unknown as { backdropRef: unknown }).backdropRef = signal({
61
+ nativeElement: view,
62
+ });
63
+ (inst as unknown as { panelRef: unknown }).panelRef = signal({
64
+ nativeElement: view,
65
+ });
66
+
67
+ inst.open.set(true);
68
+ await waitForUpdate();
69
+ await waitForUpdate();
70
+ await waitForUpdate();
71
+
72
+ inst.open.set(false);
73
+ await new Promise((r) => setTimeout(r, 320));
74
+ await waitForUpdate();
75
+ expect(inst.open()).toBe(false);
76
+ });
77
+
78
+ it('open effect bails when backdrop/panel refs are undefined', async () => {
79
+ // Covers the early-return branches in #animateIn / #animateOut.
80
+ const { componentRef } = await render(UiAlertDialog);
81
+ const inst = componentRef.instance as UiAlertDialog;
82
+ inst.open.set(true);
83
+ await waitForUpdate();
84
+ await waitForUpdate();
85
+ inst.open.set(false);
86
+ await waitForUpdate();
87
+ expect(inst.open()).toBe(false);
88
+ });
89
+
90
+ it('overlayStyle switches between visible and hidden variants', async () => {
91
+ const { componentRef, container } = await render(UiAlertDialog);
92
+ const inst = componentRef.instance as UiAlertDialog;
93
+ let overlay = container.querySelector('overlay');
94
+ expect(overlay?.getAttribute('style')).toContain('display: none');
95
+
96
+ const view = container.querySelector('view') as Element;
97
+ (inst as unknown as { backdropRef: unknown }).backdropRef = signal({
98
+ nativeElement: view,
99
+ });
100
+ (inst as unknown as { panelRef: unknown }).panelRef = signal({
101
+ nativeElement: view,
102
+ });
103
+ inst.open.set(true);
104
+ for (let i = 0; i < 10; i++) await waitForUpdate();
105
+ overlay = container.querySelector('overlay');
106
+ expect(overlay?.getAttribute('style')).not.toContain('display: none');
107
+ });
108
+
109
+ it('accepts a user class on the panel', async () => {
110
+ const { componentRef, container } = await render(UiAlertDialog);
111
+ const inst = componentRef.instance as UiAlertDialog;
112
+ setInputSignal(inst.userClass, 'custom-panel');
113
+ await waitForUpdate();
114
+ const views = Array.from(container.querySelectorAll('view'));
115
+ expect(
116
+ views.some((el) => el.getAttribute('class')?.includes('custom-panel')),
117
+ ).toBe(true);
118
+ });
119
+ });
120
+
121
+ describe('Alert dialog sub-components', () => {
122
+ it('UiAlertDialogHeader renders projected content', async () => {
123
+ const { container } = await render(UiAlertDialogHeader);
124
+ expect(container.querySelector('view')).toBeTruthy();
125
+ });
126
+
127
+ it('UiAlertDialogTitle renders projected content', async () => {
128
+ const { container } = await render(UiAlertDialogTitle);
129
+ expect(container.querySelector('text')).toBeTruthy();
130
+ });
131
+
132
+ it('UiAlertDialogDescription renders projected content', async () => {
133
+ const { container } = await render(UiAlertDialogDescription);
134
+ expect(container.querySelector('text')).toBeTruthy();
135
+ });
136
+
137
+ it('UiAlertDialogFooter renders projected content', async () => {
138
+ const { container } = await render(UiAlertDialogFooter);
139
+ expect(container.querySelector('view')).toBeTruthy();
140
+ });
141
+ });