@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,83 @@
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
+
12
+ const LOADER_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2v4"/><path d="m16.2 7.8 2.9-2.9"/><path d="M18 12h4"/><path d="m16.2 16.2 2.9 2.9"/><path d="M12 18v4"/><path d="m4.9 19.1 2.9-2.9"/><path d="M2 12h4"/><path d="m4.9 4.9 2.9 2.9"/></svg>`;
13
+
14
+ const SIZE_MAP = { xs: 16, sm: 20, md: 24, lg: 32 } as const;
15
+
16
+ @Component({
17
+ selector: 'ui-spinner',
18
+ standalone: true,
19
+ imports: [LYNX_ELEMENTS],
20
+ encapsulation: ViewEncapsulation.None,
21
+ // SVG markup is passed via [attr.content] rather than as child elements.
22
+ // Lynx's native <svg> element can't contain child nodes in the flat element
23
+ // model — the renderer doesn't support nested SVG elements. Passing the full
24
+ // SVG markup string as the `content` attribute is the Lynx-native way to
25
+ // embed vector graphics.
26
+ template: `
27
+ <view #spinner [style]="sizeStyle()">
28
+ <svg [attr.content]="svgContent()" style="width: 100%; height: 100%;" />
29
+ </view>
30
+ `,
31
+ })
32
+ export class UiSpinner {
33
+ readonly size = input<'xs' | 'sm' | 'md' | 'lg'>('md');
34
+ readonly color = input<string | undefined>(undefined);
35
+
36
+ readonly spinnerRef = viewChild<ElementRef>('spinner');
37
+
38
+ constructor() {
39
+ effect(() => {
40
+ const el = this.spinnerRef()?.nativeElement;
41
+ // el.animate() is inert in jsdom (see animate.ts's transitionTransform
42
+ // rationale). The continuous rotation is exercised on-device.
43
+ /* v8 ignore start */
44
+ if (!el) return;
45
+ el.animate(
46
+ [{ transform: 'rotate(0deg)' }, { transform: 'rotate(360deg)' }],
47
+ {
48
+ duration: 1600,
49
+ iterations: Infinity,
50
+ easing: 'linear',
51
+ },
52
+ );
53
+ /* v8 ignore stop */
54
+ });
55
+ }
56
+
57
+ protected readonly svgContent = computed(() => {
58
+ const c = this.color();
59
+ if (c) {
60
+ // Lynx SVG elements don't inherit the CSS `color` property into their
61
+ // stroke attributes, so we substitute the literal `currentColor` in the
62
+ // SVG string rather than relying on `currentColor` propagation via CSS.
63
+ //
64
+ // Uses split().join() instead of String.prototype.replaceAll(). replaceAll
65
+ // is ES2021, but Lynx's main thread runs on PrimJS at an ES2019 target;
66
+ // because it's a runtime method (not syntax) the build's down-leveling does
67
+ // not polyfill it, so it simply doesn't exist there. Calling it throws
68
+ // "main-thread.js exception: not a function" from inside Angular change
69
+ // detection, which aborts the whole tick before Lynx flushes the element
70
+ // tree — so the spinner never paints, and the error count climbs on every
71
+ // render as each tick re-runs and re-throws. split/join is ES2019-safe and
72
+ // is preferred over `replace(/.../g)` here because the search term is a
73
+ // plain literal and needs no regex escaping.
74
+ return LOADER_SVG.split('currentColor').join(c);
75
+ }
76
+ return LOADER_SVG;
77
+ });
78
+
79
+ protected readonly sizeStyle = computed(() => {
80
+ const px = SIZE_MAP[this.size()];
81
+ return `width: ${px}px; height: ${px}px;`;
82
+ });
83
+ }
@@ -0,0 +1 @@
1
+ export { UiSwitch } from './switch';
@@ -0,0 +1,63 @@
1
+ import { render, waitForUpdate } from '@blotch/angular-lynx-testing-library';
2
+ import { describe, expect, it, vi } from 'vitest';
3
+ import { setInputSignal } from '../../../test-utils/set-input-signal';
4
+ import { UiSwitch } from './switch';
5
+
6
+ describe('UiSwitch', () => {
7
+ it('renders in the off position by default', async () => {
8
+ const { container } = await render(UiSwitch);
9
+ expect(container).toBeTruthy();
10
+ });
11
+
12
+ it('toggle() flips checked and emits `changed`', async () => {
13
+ const { componentRef } = await render(UiSwitch);
14
+ const inst = componentRef.instance as UiSwitch;
15
+ const emitted: boolean[] = [];
16
+ inst.changed.subscribe((v) => emitted.push(v));
17
+
18
+ inst.toggle();
19
+ expect(inst.checked()).toBe(true);
20
+ inst.toggle();
21
+ expect(inst.checked()).toBe(false);
22
+ expect(emitted).toEqual([true, false]);
23
+ });
24
+
25
+ it('toggle() is a no-op when disabled', async () => {
26
+ const { componentRef } = await render(UiSwitch);
27
+ const inst = componentRef.instance as UiSwitch;
28
+ setInputSignal(inst.disabled, true);
29
+ const listener = vi.fn();
30
+ inst.changed.subscribe(listener);
31
+
32
+ inst.toggle();
33
+ expect(inst.checked()).toBe(false);
34
+ expect(listener).not.toHaveBeenCalled();
35
+ });
36
+
37
+ it('positions the thumb differently in ON vs OFF state', async () => {
38
+ const { componentRef, container } = await render(UiSwitch);
39
+ const inst = componentRef.instance as UiSwitch;
40
+ let thumb = container.querySelectorAll('view')[1];
41
+ expect(thumb?.getAttribute('style')).toContain('translateX(2px)');
42
+
43
+ inst.checked.set(true);
44
+ await waitForUpdate();
45
+ thumb = container.querySelectorAll('view')[1];
46
+ expect(thumb?.getAttribute('style')).toContain('translateX(22px)');
47
+ });
48
+
49
+ it('applies opacity-50 when disabled and includes a user class', async () => {
50
+ // Covers the disabled=true branch of trackClass() (line 62) and the
51
+ // userClass() concatenation. Without a render + detectChanges cycle the
52
+ // computed never runs with disabled=true and the branch stays uncovered.
53
+ const { componentRef, container } = await render(UiSwitch);
54
+ const inst = componentRef.instance as UiSwitch;
55
+ setInputSignal(inst.disabled, true);
56
+ setInputSignal(inst.userClass, 'custom-cls');
57
+ await waitForUpdate();
58
+ const track = container.querySelectorAll('view')[0];
59
+ const cls = track?.getAttribute('class') ?? '';
60
+ expect(cls).toContain('opacity-50');
61
+ expect(cls).toContain('custom-cls');
62
+ });
63
+ });
@@ -0,0 +1,98 @@
1
+ import {
2
+ type ElementRef,
3
+ Component,
4
+ ViewEncapsulation,
5
+ computed,
6
+ input,
7
+ model,
8
+ output,
9
+ viewChild,
10
+ } from '@angular/core';
11
+ import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
12
+
13
+ import { type AnimationHandle, springTranslateX } from '../../utils/animate';
14
+ import { cn } from '../../utils/cn';
15
+
16
+ /**
17
+ * Pixel offsets for the thumb within the 44px track.
18
+ *
19
+ * The thumb is positioned with a px-based `translateX`, so the track and thumb
20
+ * MUST be sized in px too (see the `w-[44px]`/`w-[20px]` arbitrary values in the
21
+ * templates below) — NOT with Tailwind's rem-based scale (`w-11`/`w-5`).
22
+ *
23
+ * Why: Tailwind's default width scale compiles to rem (`w-11` → `2.75rem`), and
24
+ * on Lynx `1rem` resolves against the root `<page>` font-size, which defaults to
25
+ * 14px (`DEFAULT_FONT_SIZE_DP`) — not the 16px browsers use. So `w-11` renders as
26
+ * 38.5px on device, not 44px. Mixing that rem-sized track with a px `translateX`
27
+ * made the ON thumb overshoot the right edge (it was offset 22px inside a 38.5px
28
+ * track). Sizing everything in px keeps the geometry self-consistent regardless
29
+ * of the ambient font-size.
30
+ *
31
+ * Track is 44px wide, 24px tall; thumb is 20px square. OFF: 2px from the left
32
+ * edge; ON: 2px from the right edge (44 - 20 - 2 = 22).
33
+ */
34
+ const THUMB_OFFSET_OFF = 2;
35
+ const THUMB_OFFSET_ON = 22;
36
+
37
+ @Component({
38
+ selector: 'ui-switch',
39
+ standalone: true,
40
+ imports: [LYNX_ELEMENTS],
41
+ encapsulation: ViewEncapsulation.None,
42
+ template: `
43
+ <view [class]="trackClass()" (bindtap)="toggle()">
44
+ <view #thumb [class]="thumbClass()" [style]="thumbPositionStyle()" />
45
+ </view>
46
+ `,
47
+ })
48
+ export class UiSwitch {
49
+ readonly checked = model(false);
50
+ readonly disabled = input(false);
51
+ readonly userClass = input<string>('', { alias: 'class' });
52
+
53
+ readonly changed = output<boolean>();
54
+
55
+ readonly thumbRef = viewChild<ElementRef>('thumb');
56
+ #activeAnimation?: AnimationHandle;
57
+
58
+ protected readonly trackClass = computed(() =>
59
+ cn(
60
+ 'h-[24px] w-[44px] flex-row items-center rounded-full flex',
61
+ this.checked() ? 'bg-primary' : 'bg-input',
62
+ this.disabled() && 'opacity-50',
63
+ this.userClass(),
64
+ ),
65
+ );
66
+
67
+ protected readonly thumbClass = computed(() =>
68
+ cn('h-[20px] w-[20px] rounded-full bg-background'),
69
+ );
70
+
71
+ protected readonly thumbPositionStyle = computed(
72
+ () =>
73
+ `transform: translateX(${this.checked() ? THUMB_OFFSET_ON : THUMB_OFFSET_OFF}px);`,
74
+ );
75
+
76
+ toggle(): void {
77
+ if (this.disabled()) return;
78
+
79
+ const thumb = this.thumbRef()?.nativeElement;
80
+ const wasChecked = this.checked();
81
+ const next = !wasChecked;
82
+
83
+ // springTranslateX calls thumb.animate() which is inert in jsdom (see
84
+ // animate.ts). The spring physics of the thumb is exercised on-device.
85
+ /* v8 ignore start */
86
+ if (thumb) {
87
+ this.#activeAnimation?.cancel();
88
+ const from = wasChecked ? THUMB_OFFSET_ON : THUMB_OFFSET_OFF;
89
+ const to = next ? THUMB_OFFSET_ON : THUMB_OFFSET_OFF;
90
+ // Spring translate with squish effect at midpoint
91
+ this.#activeAnimation = springTranslateX(thumb, from, to);
92
+ }
93
+ /* v8 ignore stop */
94
+
95
+ this.checked.set(next);
96
+ this.changed.emit(next);
97
+ }
98
+ }
@@ -0,0 +1 @@
1
+ export { UiTabs, UiTabsList, UiTabsTrigger, UiTabsContent } from './tabs';
@@ -0,0 +1,199 @@
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 { UiTabs, UiTabsContent, UiTabsList, UiTabsTrigger } from './tabs';
7
+
8
+ @Component({
9
+ standalone: true,
10
+ imports: [UiTabs, UiTabsList, UiTabsTrigger, UiTabsContent, LYNX_ELEMENTS],
11
+ template: `
12
+ <ui-tabs [(value)]="value">
13
+ <ui-tabs-list>
14
+ <ui-tabs-trigger value="a">A</ui-tabs-trigger>
15
+ <ui-tabs-trigger value="b">B</ui-tabs-trigger>
16
+ </ui-tabs-list>
17
+ <ui-tabs-content value="a"><text>Panel A</text></ui-tabs-content>
18
+ <ui-tabs-content value="b"><text>Panel B</text></ui-tabs-content>
19
+ </ui-tabs>
20
+ `,
21
+ })
22
+ class TabsHost {
23
+ value = 'a';
24
+ }
25
+
26
+ describe('UiTabs', () => {
27
+ it('renders the tab triggers and the active panel', async () => {
28
+ const { getByText } = await render(TabsHost);
29
+ expect(getByText('A')).toBeTruthy();
30
+ expect(getByText('B')).toBeTruthy();
31
+ expect(getByText('Panel A')).toBeTruthy();
32
+ });
33
+
34
+ it('select() sets the model value and emits `changed`', async () => {
35
+ const { componentRef } = await render(UiTabs);
36
+ const inst = componentRef.instance as UiTabs;
37
+ const emitted: string[] = [];
38
+ inst.changed.subscribe((v) => emitted.push(v));
39
+ inst.select('x');
40
+ expect(inst.value()).toBe('x');
41
+ expect(emitted).toEqual(['x']);
42
+ });
43
+
44
+ it('registerTab() records tab order without duplicates', async () => {
45
+ const { componentRef } = await render(UiTabs);
46
+ const inst = componentRef.instance as UiTabs;
47
+ inst.registerTab('a');
48
+ inst.registerTab('b');
49
+ inst.registerTab('a'); // duplicate, should be ignored
50
+ // Seed the effect with a known previousValue so subsequent transitions
51
+ // exercise the direction-set branch (not the initial-run early return).
52
+ inst.value.set('a');
53
+ await waitForUpdate();
54
+ // a=0, b=1 → nextIndex > prevIndex → direction=1 (forward branch)
55
+ inst.value.set('b');
56
+ await waitForUpdate();
57
+ await waitForUpdate();
58
+ expect(inst.direction()).toBe(1);
59
+ // Reverse → direction=-1 (backward branch)
60
+ inst.value.set('a');
61
+ await waitForUpdate();
62
+ await waitForUpdate();
63
+ expect(inst.direction()).toBe(-1);
64
+ });
65
+
66
+ it('direction effect ignores unknown values', async () => {
67
+ // Covers the branch where prevIndex or nextIndex is -1.
68
+ const { componentRef } = await render(UiTabs);
69
+ const inst = componentRef.instance as UiTabs;
70
+ inst.value.set('unknown');
71
+ inst.value.set('also-unknown');
72
+ await waitForUpdate();
73
+ await waitForUpdate();
74
+ expect(inst.direction()).toBe(1); // unchanged from default
75
+ });
76
+
77
+ it('direction effect leaves direction alone when transitioning from unknown to known', async () => {
78
+ const { componentRef } = await render(UiTabs);
79
+ const inst = componentRef.instance as UiTabs;
80
+ inst.registerTab('a');
81
+ inst.registerTab('b');
82
+ inst.value.set('unknown');
83
+ await waitForUpdate();
84
+ inst.value.set('a');
85
+ await waitForUpdate();
86
+ expect(inst.direction()).toBe(1);
87
+ });
88
+
89
+ it('direction effect leaves direction alone when transitioning from known to unknown', async () => {
90
+ const { componentRef } = await render(UiTabs);
91
+ const inst = componentRef.instance as UiTabs;
92
+ inst.registerTab('a');
93
+ inst.registerTab('b');
94
+ inst.value.set('a');
95
+ await waitForUpdate();
96
+ inst.value.set('unknown');
97
+ await waitForUpdate();
98
+ expect(inst.direction()).toBe(1);
99
+ });
100
+ });
101
+
102
+ describe('UiTabsList', () => {
103
+ it('renders projected content with the list container class', async () => {
104
+ const { container } = await render(UiTabsList);
105
+ const view = container.querySelector('view');
106
+ expect(view?.getAttribute('class')).toContain('rounded-lg');
107
+ });
108
+ });
109
+
110
+ describe('UiTabsTrigger', () => {
111
+ const renderTrigger = async (activeValue = 'a') => {
112
+ const tabsResult = await render(UiTabs);
113
+ const tabs = tabsResult.componentRef.instance as UiTabs;
114
+ tabs.value.set(activeValue);
115
+
116
+ const triggerResult = await render(UiTabsTrigger, {
117
+ providers: [{ provide: UiTabs, useValue: tabs }],
118
+ });
119
+ const trigger = triggerResult.componentRef.instance as UiTabsTrigger;
120
+ setInputSignal(trigger.triggerValue, 'a');
121
+ const view = triggerResult.container.querySelector('view') as Element;
122
+ (trigger as unknown as { triggerRef: unknown }).triggerRef = signal({
123
+ nativeElement: view,
124
+ });
125
+ await waitForUpdate();
126
+ await waitForUpdate();
127
+ return { tabs, trigger, triggerResult };
128
+ };
129
+
130
+ it('select() delegates to the parent tabs', async () => {
131
+ const { tabs, trigger } = await renderTrigger('a');
132
+ trigger.select();
133
+ expect(tabs.value()).toBe('a');
134
+ });
135
+
136
+ it('effect runs the activation animation when active flips true', async () => {
137
+ // Active path — `#previousActive` starts undefined so the first flip
138
+ // exercises the initialization guard, and toggling activeValue drives the
139
+ // effect through both animate() branches.
140
+ const { tabs } = await renderTrigger('b');
141
+ // Switch tabs so trigger becomes active — activation branch.
142
+ tabs.value.set('a');
143
+ await waitForUpdate();
144
+ await waitForUpdate();
145
+ // Deactivation branch.
146
+ tabs.value.set('b');
147
+ await waitForUpdate();
148
+ await waitForUpdate();
149
+ expect(tabs.value()).toBe('b');
150
+ });
151
+ });
152
+
153
+ describe('UiTabsContent', () => {
154
+ it('runs the reveal + directional slide animations when active flips', async () => {
155
+ const tabsResult = await render(UiTabs);
156
+ const tabs = tabsResult.componentRef.instance as UiTabs;
157
+ tabs.registerTab('a');
158
+ tabs.registerTab('b');
159
+
160
+ const contentResult = await render(UiTabsContent, {
161
+ providers: [{ provide: UiTabs, useValue: tabs }],
162
+ });
163
+ const content = contentResult.componentRef.instance as UiTabsContent;
164
+ setInputSignal(content.contentValue, 'a');
165
+ const view = contentResult.container.querySelector('view') as Element;
166
+ (content as unknown as { contentRef: unknown }).contentRef = signal({
167
+ nativeElement: view,
168
+ });
169
+
170
+ // Activate — first render path (revealIn).
171
+ tabs.value.set('a');
172
+ await waitForUpdate();
173
+ // The effect schedules setTimeout(_, 0); wait one more tick for it.
174
+ await waitForUpdate();
175
+
176
+ // Switch off and back on — subsequent path (directionalSlideIn) with cancel.
177
+ tabs.value.set('b');
178
+ await waitForUpdate();
179
+ tabs.value.set('a');
180
+ await waitForUpdate();
181
+ await waitForUpdate();
182
+
183
+ expect(tabs.value()).toBe('a');
184
+ });
185
+
186
+ it('setTimeout body bails out when contentRef is undefined', async () => {
187
+ const tabsResult = await render(UiTabs);
188
+ const tabs = tabsResult.componentRef.instance as UiTabs;
189
+ const contentResult = await render(UiTabsContent, {
190
+ providers: [{ provide: UiTabs, useValue: tabs }],
191
+ });
192
+ const content = contentResult.componentRef.instance as UiTabsContent;
193
+ setInputSignal(content.contentValue, 'a');
194
+ tabs.value.set('a');
195
+ await waitForUpdate();
196
+ await waitForUpdate();
197
+ expect(tabs.value()).toBe('a');
198
+ });
199
+ });