@blotch/dolan 0.0.0-stage → 22.2.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 (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,504 @@
1
+ import {
2
+ type ElementRef,
3
+ Component,
4
+ ViewEncapsulation,
5
+ computed,
6
+ contentChildren,
7
+ effect,
8
+ inject,
9
+ input,
10
+ model,
11
+ output,
12
+ signal,
13
+ viewChild,
14
+ } from '@angular/core';
15
+ import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
16
+
17
+ import {
18
+ type AnimationHandle,
19
+ DURATION,
20
+ EASING,
21
+ SCALE,
22
+ fadeIn,
23
+ fadeOut,
24
+ pressDown,
25
+ pressRelease,
26
+ slideIn,
27
+ slideOut,
28
+ } from '../../utils/animate';
29
+ import { cn } from '../../utils/cn';
30
+ import { UiIcon } from '../icon';
31
+
32
+ @Component({
33
+ selector: 'ui-nav-drawer',
34
+ standalone: true,
35
+ imports: [LYNX_ELEMENTS],
36
+ encapsulation: ViewEncapsulation.None,
37
+ template: `
38
+ <overlay [attr.visible]="overlayVisible()" [style]="overlayStyle()">
39
+ <!-- bg-black/50 dims the content behind the drawer. The fadeIn/fadeOut in
40
+ #animateIn/#animateOut already tween this element's opacity over
41
+ DURATION.normal, so the dim animates in lockstep with the panel slide —
42
+ matching the dialog/action-sheet/bottom-sheet backdrop. A named color
43
+ with an opacity modifier is required here: bg-black/50 compiles to
44
+ rgb(0 0 0 / 0.5), which Lynx accepts, whereas opacity modifiers on
45
+ semantic tokens don't. -->
46
+ <view
47
+ #backdrop
48
+ class="h-full w-full bg-black/50"
49
+ (bindtap)="onBackdropTap()"
50
+ >
51
+ <view
52
+ #panel
53
+ [class]="panelClass()"
54
+ [style]="panelPositionStyle()"
55
+ (catchtap)="onPanelTap()"
56
+ >
57
+ <ng-content />
58
+ </view>
59
+ </view>
60
+ </overlay>
61
+ `,
62
+ })
63
+ export class UiNavDrawer {
64
+ readonly open = model(false);
65
+ readonly side = input<'left' | 'right'>('left');
66
+ readonly userClass = input<string>('', { alias: 'class' });
67
+
68
+ readonly closed = output<void>();
69
+
70
+ protected readonly overlayVisible = signal(false);
71
+ /**
72
+ * `width`/`height`/`top`/`left` are web-only necessities: on web
73
+ * `<overlay>` has no intrinsic full-screen sizing (unlike native), so
74
+ * without them it collapses to 0x0 and nothing renders. `z-index` keeps it
75
+ * above sibling content that would otherwise occasionally paint on top.
76
+ */
77
+ protected readonly overlayStyle = computed(() =>
78
+ this.overlayVisible()
79
+ ? 'position: fixed; overflow: visible; width: 100vw; height: 100vh; top: 0; left: 0; z-index: 999;'
80
+ : 'position: fixed; overflow: visible; width: 100vw; height: 100vh; top: 0; left: 0; z-index: 999; display: none;',
81
+ );
82
+
83
+ readonly backdropRef = viewChild<ElementRef>('backdrop');
84
+ readonly panelRef = viewChild<ElementRef>('panel');
85
+ #backdropAnim?: AnimationHandle;
86
+ #panelAnim?: AnimationHandle;
87
+ #hasBeenOpen = false;
88
+
89
+ constructor() {
90
+ // `#hasBeenOpen` prevents the close animation from running on the initial
91
+ // effect evaluation when `open` starts as false. Without this guard, the
92
+ // first run would call #doClose() and emit `closed` before the drawer
93
+ // has ever been opened.
94
+ effect(() => {
95
+ const isOpen = this.open();
96
+ if (isOpen) {
97
+ this.#hasBeenOpen = true;
98
+ this.#doOpen();
99
+ } else if (this.#hasBeenOpen) {
100
+ this.#doClose();
101
+ }
102
+ });
103
+ }
104
+
105
+ protected readonly panelClass = computed(() =>
106
+ cn(
107
+ 'h-full flex-col bg-background flex',
108
+ this.side() === 'left'
109
+ ? 'border-r border-border'
110
+ : 'border-l border-border',
111
+ this.userClass(),
112
+ ),
113
+ );
114
+
115
+ protected readonly panelPositionStyle = computed(() => {
116
+ const sideValue = this.side();
117
+ return sideValue === 'left'
118
+ ? 'position: absolute; top: 0; bottom: 0; left: 0; width: 80%;'
119
+ : 'position: absolute; top: 0; bottom: 0; right: 0; width: 80%;';
120
+ });
121
+
122
+ protected onBackdropTap(): void {
123
+ this.open.set(false);
124
+ }
125
+
126
+ /**
127
+ * No-op tap handler for the panel. `catchtap` (vs `bindtap`) already stops
128
+ * the tap from bubbling to the backdrop — Lynx controls propagation via the
129
+ * event prefix, not at runtime. (The renderer shims `event.stopPropagation()`
130
+ * as a no-op so DOM-style handlers don't crash, but it has no effect here.)
131
+ */
132
+ protected onPanelTap(): void {}
133
+
134
+ close(): void {
135
+ this.open.set(false);
136
+ }
137
+
138
+ /**
139
+ * Two-phase open: first make the overlay visible (so native elements exist),
140
+ * then animate in on the next frame. Without the nested setTimeout, the
141
+ * animate() call would target elements that haven't been flushed to native
142
+ * yet — Lynx element.animate() requires the element to be in the tree.
143
+ */
144
+ #doOpen(): void {
145
+ setTimeout(() => {
146
+ this.overlayVisible.set(true);
147
+ setTimeout(() => this.#animateIn(), 0);
148
+ }, 0);
149
+ }
150
+
151
+ /**
152
+ * Reverse of open: animate out first, then hide the overlay after the
153
+ * animation completes. The +20ms buffer accounts for timing imprecision
154
+ * in Lynx's timer system — removing the overlay mid-animation would
155
+ * cause a visual glitch (elements disappear before fade completes).
156
+ */
157
+ #doClose(): void {
158
+ setTimeout(() => {
159
+ this.#animateOut();
160
+ setTimeout(() => {
161
+ this.overlayVisible.set(false);
162
+ this.closed.emit();
163
+ }, DURATION.normal + 20);
164
+ }, 0);
165
+ }
166
+
167
+ #animateIn(): void {
168
+ const backdrop = this.backdropRef()?.nativeElement;
169
+ const panel = this.panelRef()?.nativeElement;
170
+ if (!backdrop || !panel) return;
171
+
172
+ this.#backdropAnim?.cancel();
173
+ this.#panelAnim?.cancel();
174
+
175
+ this.#backdropAnim = fadeIn(backdrop, { duration: DURATION.normal });
176
+ const direction = this.side() === 'left' ? 'left' : 'right';
177
+ this.#panelAnim = slideIn(panel, direction, {
178
+ duration: DURATION.slow,
179
+ easing: EASING.sheet,
180
+ });
181
+ }
182
+
183
+ #animateOut(): void {
184
+ const backdrop = this.backdropRef()?.nativeElement;
185
+ const panel = this.panelRef()?.nativeElement;
186
+ if (!backdrop || !panel) return;
187
+
188
+ this.#backdropAnim?.cancel();
189
+ this.#panelAnim?.cancel();
190
+
191
+ this.#backdropAnim = fadeOut(backdrop, { duration: DURATION.normal });
192
+ const direction = this.side() === 'left' ? 'left' : 'right';
193
+ this.#panelAnim = slideOut(panel, direction, {
194
+ duration: DURATION.normal,
195
+ easing: EASING.accelerate,
196
+ });
197
+ }
198
+ }
199
+
200
+ @Component({
201
+ selector: 'ui-nav-drawer-header',
202
+ standalone: true,
203
+ imports: [LYNX_ELEMENTS],
204
+ encapsulation: ViewEncapsulation.None,
205
+ template: `
206
+ <view [class]="containerClass()">
207
+ <ng-content />
208
+ </view>
209
+ `,
210
+ })
211
+ export class UiNavDrawerHeader {
212
+ readonly userClass = input<string>('', { alias: 'class' });
213
+
214
+ // Fixes: the header title/subtitle were drawn behind the device status bar /
215
+ // notch / dynamic island. The drawer panel is positioned top: 0; bottom: 0
216
+ // (see UiNavDrawer.panelPositionStyle), so it spans the full screen height and
217
+ // the header — its first child — starts at y=0, under the system inset.
218
+ // Fix: split the padding — keep `px-4 pb-4` (1rem) and swap the top for
219
+ // `pt-safe-4` = calc(1rem + env(safe-area-inset-top)), preserving the original
220
+ // p-4 top padding while clearing the inset. Degrades to plain 1rem where the
221
+ // inset is 0 (e.g. the web preview), so no platform fork is needed.
222
+ protected readonly containerClass = computed(() =>
223
+ cn(
224
+ 'flex-col gap-1.5 border-b border-border px-4 pb-4 flex pt-safe-4',
225
+ this.userClass(),
226
+ ),
227
+ );
228
+ }
229
+
230
+ @Component({
231
+ selector: 'ui-nav-drawer-content',
232
+ standalone: true,
233
+ imports: [LYNX_ELEMENTS],
234
+ encapsulation: ViewEncapsulation.None,
235
+ // Fixes: a long item list scrolled nowhere and spilled over the header and
236
+ // footer instead of scrolling within its own section.
237
+ //
238
+ // Root cause: Lynx forces `scroll-view` into linear layout, where it EXPANDS
239
+ // to fit its content rather than accepting a flex-computed height (see
240
+ // investigations/lynx-vs-web-differences.md, "scroll-view requires an explicit
241
+ // height constraint to scroll"). It therefore has no bound to scroll within
242
+ // and grows past its slot in the panel's flex-col.
243
+ //
244
+ // Two approaches were tried and failed, hence this shape:
245
+ // 1. `flex-1` directly on the scroll-view — ignored, because the scroll-view
246
+ // sizes to content, not to the flex track. Still overflowed.
247
+ // 2. wrapping the scroll-view in an inner `flex-1` view — the inner view is
248
+ // NOT the flex child of the panel; the host element is. The host still
249
+ // sized to content and overflowed, and the wrapper being `flex` (a ROW)
250
+ // also shrank the scroll-view to its content WIDTH.
251
+ //
252
+ // Working fix: put the flex sizing on the HOST element (the real flex child of
253
+ // the panel's flex-col — same lesson as ui-tabs-content / ui-separator).
254
+ // `flex-1` claims the space left between the header and footer; `flex flex-col`
255
+ // makes the host lay the scroll-view out vertically to fill that height. The
256
+ // panel's flex-col stretches the host to full width by default (Lynx
257
+ // align-items: stretch), so no width class is needed on the host.
258
+ host: {
259
+ class: 'flex-1 flex-col flex',
260
+ },
261
+ // With the host now a definite height, the scroll-view fills it with `h-full`
262
+ // (the explicit height Lynx requires to actually scroll) and `w-full` to span
263
+ // the host width. This is the bound the scroll-view was missing above.
264
+ //
265
+ // (bindcontentsizechanged) fires once the scroll-view finishes laying out its
266
+ // content (iOS/Android). That's the exact moment child frames exist, so it's
267
+ // the precise trigger for scrolling the active item into view when the drawer
268
+ // is first shown — see onContentLayout().
269
+ template: `
270
+ <scroll-view
271
+ scroll-orientation="vertical"
272
+ [class]="contentClass()"
273
+ (bindcontentsizechanged)="onContentLayout()"
274
+ >
275
+ <ng-content />
276
+ </scroll-view>
277
+ `,
278
+ })
279
+ export class UiNavDrawerContent {
280
+ readonly #drawer = inject(UiNavDrawer);
281
+
282
+ readonly userClass = input<string>('', { alias: 'class' });
283
+
284
+ // Every nav item projected into this section (Home + each group item). Used to
285
+ // find the active one when the drawer opens. `descendants: true` reaches items
286
+ // nested inside the @for group wrappers, not just the direct content children.
287
+ readonly items = contentChildren(UiNavDrawerItem, { descendants: true });
288
+
289
+ protected readonly contentClass = computed(() =>
290
+ cn('h-full w-full', this.userClass()),
291
+ );
292
+
293
+ constructor() {
294
+ // When the drawer opens, land the user on their current route instead of the
295
+ // top of a potentially long nav list. The <overlay> is display:none until
296
+ // open, so the scroll-view has no laid-out child frames until it's shown —
297
+ // defer a tick so the just-shown content is measured before we read it.
298
+ //
299
+ // This open-driven path is the fallback: it covers the web preview (where
300
+ // contentsizechanged isn't emitted) and re-opens that don't trigger a
301
+ // relayout. On device, onContentLayout() below is the precise trigger. Both
302
+ // call the same instant scroll, so firing twice is a harmless no-op.
303
+ effect(() => {
304
+ if (!this.#drawer.open()) return;
305
+ setTimeout(() => this.#scrollActiveIntoView(), 0);
306
+ });
307
+ }
308
+
309
+ /**
310
+ * Fired by the scroll-view once its content finishes layout. Gated on the
311
+ * drawer being open so a close-time relayout (content collapsing as the overlay
312
+ * hides) doesn't scroll an already-dismissed panel.
313
+ */
314
+ protected onContentLayout(): void {
315
+ if (this.#drawer.open()) this.#scrollActiveIntoView();
316
+ }
317
+
318
+ #scrollActiveIntoView(): void {
319
+ this.items()
320
+ /* v8 ignore start -- .find predicate unreachable when no content-projected items */
321
+ .find((item) => item.active())
322
+ /* v8 ignore stop */
323
+ ?.scrollIntoView();
324
+ }
325
+ }
326
+
327
+ @Component({
328
+ selector: 'ui-nav-drawer-item',
329
+ standalone: true,
330
+ imports: [LYNX_ELEMENTS],
331
+ encapsulation: ViewEncapsulation.None,
332
+ template: `
333
+ <view
334
+ #container
335
+ [class]="containerClass()"
336
+ (bindtouchstart)="onPressStart()"
337
+ (bindtouchend)="onPressEnd()"
338
+ (bindtouchcancel)="onPressCancel()"
339
+ (bindmousedown)="onPressStart()"
340
+ (bindmouseup)="onPressEnd()"
341
+ (bindmouseleave)="onPressCancel()"
342
+ (bindtap)="onTap()"
343
+ >
344
+ <ng-content />
345
+ </view>
346
+ `,
347
+ })
348
+ export class UiNavDrawerItem {
349
+ readonly #drawer = inject(UiNavDrawer);
350
+
351
+ readonly variant = input<'default' | 'destructive'>('default');
352
+ readonly active = input(false);
353
+ readonly autoClose = input(true);
354
+ readonly userClass = input<string>('', { alias: 'class' });
355
+
356
+ readonly pressed = output<void>();
357
+
358
+ readonly containerRef = viewChild<ElementRef>('container');
359
+ #pressAnim?: AnimationHandle;
360
+
361
+ protected readonly containerClass = computed(() =>
362
+ cn(
363
+ 'flex-row items-center gap-3 px-4 py-3 flex',
364
+ this.active() ? 'bg-accent' : 'bg-transparent',
365
+ this.userClass(),
366
+ ),
367
+ );
368
+
369
+ protected onPressStart(): void {
370
+ this.#pressAnim?.cancel();
371
+ this.#pressAnim = pressDown(
372
+ this.containerRef()?.nativeElement,
373
+ SCALE.pressDownLight,
374
+ );
375
+ }
376
+
377
+ protected onPressEnd(): void {
378
+ this.#pressAnim?.cancel();
379
+ this.#pressAnim = pressRelease(this.containerRef()?.nativeElement);
380
+ }
381
+
382
+ protected onPressCancel(): void {
383
+ this.#pressAnim?.cancel();
384
+ this.#pressAnim = pressRelease(this.containerRef()?.nativeElement);
385
+ }
386
+
387
+ protected onTap(): void {
388
+ this.pressed.emit();
389
+ if (this.autoClose()) {
390
+ this.#drawer.close();
391
+ }
392
+ }
393
+
394
+ /**
395
+ * Scrolls the enclosing scroll-view so this item is visible. `scrollIntoView`
396
+ * is a base LynxUI UIMethod bound to ANY (direct or indirect) child of a
397
+ * scroll container: it walks up to the nearest <scroll-view> and scrolls that
398
+ * container itself. Because it targets the element — not a child index/offset —
399
+ * it's immune to the display:none control-flow anchor <view>s Angular
400
+ * interleaves as scroll-view children for @if/@for (which would corrupt any
401
+ * index-based scroll). UiNavDrawerContent calls this to reveal the active route
402
+ * when the drawer opens.
403
+ *
404
+ * block:'center' centers the item in the viewport; behavior:'auto' positions
405
+ * instantly (not animated) so the drawer appears already-scrolled as it slides
406
+ * in, instead of visibly jumping after it settles.
407
+ */
408
+ scrollIntoView(): void {
409
+ // The param shape differs by platform, so send both — each side reads its
410
+ // own keys and ignores the rest:
411
+ // - Native Lynx (iOS/Android) reads params.scrollIntoViewOptions.
412
+ // - Web maps invoke() onto the DOM element.scrollIntoView(params), which
413
+ // reads top-level ScrollIntoViewOptions (block/behavior).
414
+ this.containerRef()?.nativeElement.invoke('scrollIntoView', {
415
+ scrollIntoViewOptions: { block: 'center', behavior: 'auto' },
416
+ block: 'center',
417
+ behavior: 'auto',
418
+ });
419
+ }
420
+ }
421
+
422
+ @Component({
423
+ selector: 'ui-nav-drawer-footer',
424
+ standalone: true,
425
+ imports: [LYNX_ELEMENTS],
426
+ encapsulation: ViewEncapsulation.None,
427
+ template: `
428
+ <view [class]="containerClass()">
429
+ <ng-content />
430
+ </view>
431
+ `,
432
+ })
433
+ export class UiNavDrawerFooter {
434
+ readonly userClass = input<string>('', { alias: 'class' });
435
+
436
+ // Fixes (mirror of the header): the footer content was drawn behind the home
437
+ // indicator. The footer is the last child of the top: 0; bottom: 0 panel, so
438
+ // its lower edge reaches the very bottom of the screen, under the inset.
439
+ // Fix: keep `px-4 pt-4` (1rem) and swap the bottom for `pb-safe-4` =
440
+ // calc(1rem + env(safe-area-inset-bottom)), preserving the original p-4 bottom
441
+ // padding while lifting the content clear of the inset.
442
+ protected readonly containerClass = computed(() =>
443
+ cn(
444
+ 'flex-col gap-2 border-t border-border px-4 pt-4 flex pb-safe-4',
445
+ this.userClass(),
446
+ ),
447
+ );
448
+ }
449
+
450
+ @Component({
451
+ selector: 'ui-nav-drawer-trigger',
452
+ standalone: true,
453
+ imports: [LYNX_ELEMENTS, UiIcon],
454
+ encapsulation: ViewEncapsulation.None,
455
+ template: `
456
+ <view
457
+ #container
458
+ [class]="containerClass()"
459
+ (bindtouchstart)="onPressStart()"
460
+ (bindtouchend)="onPressEnd()"
461
+ (bindtouchcancel)="onPressCancel()"
462
+ (bindmousedown)="onPressStart()"
463
+ (bindmouseup)="onPressEnd()"
464
+ (bindmouseleave)="onPressCancel()"
465
+ (bindtap)="onTap()"
466
+ >
467
+ <ui-icon name="menu" size="md" />
468
+ </view>
469
+ `,
470
+ })
471
+ export class UiNavDrawerTrigger {
472
+ readonly userClass = input<string>('', { alias: 'class' });
473
+
474
+ readonly pressed = output<void>();
475
+
476
+ readonly containerRef = viewChild<ElementRef>('container');
477
+ #pressAnim?: AnimationHandle;
478
+
479
+ protected readonly containerClass = computed(() =>
480
+ cn(
481
+ 'h-10 w-10 items-center rounded-md flex justify-center',
482
+ this.userClass(),
483
+ ),
484
+ );
485
+
486
+ protected onPressStart(): void {
487
+ this.#pressAnim?.cancel();
488
+ this.#pressAnim = pressDown(this.containerRef()?.nativeElement);
489
+ }
490
+
491
+ protected onPressEnd(): void {
492
+ this.#pressAnim?.cancel();
493
+ this.#pressAnim = pressRelease(this.containerRef()?.nativeElement);
494
+ }
495
+
496
+ protected onPressCancel(): void {
497
+ this.#pressAnim?.cancel();
498
+ this.#pressAnim = pressRelease(this.containerRef()?.nativeElement);
499
+ }
500
+
501
+ protected onTap(): void {
502
+ this.pressed.emit();
503
+ }
504
+ }
@@ -0,0 +1 @@
1
+ export { UiPagination } from './pagination';
@@ -0,0 +1,96 @@
1
+ import { render, waitForUpdate } from '@blotch/angular-lynx-testing-library';
2
+ import { describe, expect, it } from 'vitest';
3
+ import { setInputSignal } from '../../../test-utils/set-input-signal';
4
+ import { UiPagination } from './pagination';
5
+
6
+ describe('UiPagination', () => {
7
+ const setup = async (totalPages: number, page = 1, siblingCount = 1) => {
8
+ const { componentRef, container } = await render(UiPagination);
9
+ const inst = componentRef.instance as UiPagination;
10
+ setInputSignal(inst.totalPages, totalPages);
11
+ setInputSignal(inst.siblingCount, siblingCount);
12
+ inst.page.set(page);
13
+ await waitForUpdate();
14
+ return { inst, container };
15
+ };
16
+
17
+ it('renders the pagination controls', async () => {
18
+ const { container } = await setup(5, 2);
19
+ expect(container).toBeTruthy();
20
+ });
21
+
22
+ it('lists every page when total fits the un-truncated window', async () => {
23
+ // With siblings=1 the threshold is 5+2 = 7, so a total of 5 stays inline.
24
+ const { container } = await setup(5, 3);
25
+ // Each page should render its numeric label.
26
+ const text = container.textContent ?? '';
27
+ expect(text).toContain('1');
28
+ expect(text).toContain('5');
29
+ expect(text).not.toContain('...');
30
+ });
31
+
32
+ it('inserts a trailing ellipsis when the window is near the start', async () => {
33
+ // total=10, page=2, siblings=1 → rangeStart=2, rangeEnd=3; gap on right only.
34
+ const { container } = await setup(10, 2);
35
+ expect(container.textContent ?? '').toContain('...');
36
+ });
37
+
38
+ it('inserts a leading ellipsis when the window is near the end', async () => {
39
+ // total=10, page=9, siblings=1 → rangeStart=8, rangeEnd=9; gap on left only.
40
+ const { container } = await setup(10, 9);
41
+ expect(container.textContent ?? '').toContain('...');
42
+ });
43
+
44
+ it('inserts leading and trailing ellipses when the window sits in the middle', async () => {
45
+ // total=10, page=5, siblings=1 → rangeStart=4, rangeEnd=6; gaps on both sides.
46
+ const { container } = await setup(10, 5);
47
+ // Two ellipses in the DOM as separate <text> nodes.
48
+ expect(
49
+ container.textContent?.match(/\.\.\./g)?.length,
50
+ ).toBeGreaterThanOrEqual(1);
51
+ });
52
+
53
+ it('prev() decrements page when > 1', async () => {
54
+ const { inst } = await setup(5, 3);
55
+ (inst as unknown as { prev: () => void }).prev();
56
+ await waitForUpdate();
57
+ expect(inst.page()).toBe(2);
58
+ });
59
+
60
+ it('prev() is a no-op at page 1', async () => {
61
+ const { inst } = await setup(5, 1);
62
+ (inst as unknown as { prev: () => void }).prev();
63
+ await waitForUpdate();
64
+ expect(inst.page()).toBe(1);
65
+ });
66
+
67
+ it('next() increments page when < totalPages', async () => {
68
+ const { inst } = await setup(5, 3);
69
+ (inst as unknown as { next: () => void }).next();
70
+ await waitForUpdate();
71
+ expect(inst.page()).toBe(4);
72
+ });
73
+
74
+ it('next() is a no-op at the last page', async () => {
75
+ const { inst } = await setup(5, 5);
76
+ (inst as unknown as { next: () => void }).next();
77
+ await waitForUpdate();
78
+ expect(inst.page()).toBe(5);
79
+ });
80
+
81
+ it('goTo() jumps the page directly', async () => {
82
+ const { inst } = await setup(5, 1);
83
+ (inst as unknown as { goTo: (n: number) => void }).goTo(4);
84
+ await waitForUpdate();
85
+ expect(inst.page()).toBe(4);
86
+ });
87
+
88
+ it('active page carries the primary variant class', async () => {
89
+ const { container } = await setup(3, 2);
90
+ // The active page's tile background is bg-primary; the others are transparent.
91
+ const primary = Array.from(container.querySelectorAll('view')).find((el) =>
92
+ el.getAttribute('class')?.includes('bg-primary'),
93
+ );
94
+ expect(primary).toBeTruthy();
95
+ });
96
+ });