@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,273 @@
1
+ import {
2
+ Component,
3
+ ViewEncapsulation,
4
+ computed,
5
+ inject,
6
+ input,
7
+ output,
8
+ } from '@angular/core';
9
+ import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
10
+
11
+ import {
12
+ type ChartPadding,
13
+ type ChartPoint,
14
+ UiCartesianChart,
15
+ niceScale,
16
+ sampleSmoothLine,
17
+ } from '../cartesian-chart/cartesian-chart';
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // Pure geometry (exported so it can be unit-tested without Angular)
21
+ // ---------------------------------------------------------------------------
22
+
23
+ /** A rotated line segment spec: start point (plot-local px), length, angle. */
24
+ export type LineSegment = {
25
+ x: number;
26
+ y: number;
27
+ length: number;
28
+ /** Rotation in degrees, measured clockwise from the +x axis (screen y-down). */
29
+ angle: number;
30
+ };
31
+
32
+ /**
33
+ * Turns a polyline — already projected into plot-local pixel space — into a list
34
+ * of rotated segment specs. Lynx has no `<line>`/SVG, so each segment is drawn
35
+ * as a thin `<view>` rotated about its start point (see `UiLineSeries`).
36
+ *
37
+ * The length uses `Math.sqrt(dx² + dy²)` rather than `Math.hypot` for the same
38
+ * reason `niceNum` avoids `Math.log10`: `hypot` is a runtime method the Lynx
39
+ * build pipeline may not polyfill, whereas `sqrt`/`atan2` are ES5 and always
40
+ * present. Zero-length segments (duplicate consecutive points) are skipped so we
41
+ * never emit a degenerate 0-width rotated view.
42
+ */
43
+ export const computeLineSegments = (
44
+ points: readonly ChartPoint[],
45
+ ): LineSegment[] => {
46
+ const segments: LineSegment[] = [];
47
+ for (let i = 0; i < points.length - 1; i++) {
48
+ const p1 = points[i];
49
+ const p2 = points[i + 1];
50
+ const dx = p2.x - p1.x;
51
+ const dy = p2.y - p1.y;
52
+ const length = Math.sqrt(dx * dx + dy * dy);
53
+ if (length === 0) continue;
54
+ const angle = (Math.atan2(dy, dx) * 180) / Math.PI;
55
+ segments.push({ x: p1.x, y: p1.y, length, angle });
56
+ }
57
+ return segments;
58
+ };
59
+
60
+ // ---------------------------------------------------------------------------
61
+ // Rendering helpers (mirror the equivalents in cartesian-chart.ts — kept local
62
+ // so this module stays self-contained when scaffolded by `dolan add`)
63
+ // ---------------------------------------------------------------------------
64
+
65
+ /**
66
+ * Formats a pixel value for an inline style, avoiding scientific notation.
67
+ */
68
+ const px = (n: number): string => `${n.toFixed(2)}px`;
69
+
70
+ /**
71
+ * Default axis label formatter — plain JS only (no `Intl`, absent in Lynx).
72
+ */
73
+ const defaultTickFormat = (value: number): string =>
74
+ String(Math.round(value * 100) / 100);
75
+
76
+ /**
77
+ * A single line series drawn inside a `<ui-cartesian-chart>`. Reads the shared
78
+ * scales from the parent chart via DI and renders the polyline (rotated `<view>`
79
+ * segments) plus a dot at each data point.
80
+ *
81
+ * ## Why the host is `position: absolute; width/height: 100%`
82
+ * Lynx resolves an absolute child against its **direct parent** (there is no
83
+ * `position: static`). The marks are direct children of this host, so the host
84
+ * itself must be the plot-sized containing block — it fills the plot area (its
85
+ * direct parent, a definite-size box). Sizing an *inner* view instead would fail
86
+ * because a content-sized host of absolute-only children collapses to 0×0.
87
+ */
88
+ @Component({
89
+ selector: 'ui-line-series',
90
+ standalone: true,
91
+ imports: [LYNX_ELEMENTS],
92
+ encapsulation: ViewEncapsulation.None,
93
+ host: {
94
+ style:
95
+ 'position: absolute; left: 0px; top: 0px; width: 100%; height: 100%;',
96
+ '[class]': 'userClass()',
97
+ },
98
+ template: `
99
+ <!-- Segments render before dots so each dot paints on top of the line
100
+ (Lynx paint order follows source order; z-index is unnecessary). -->
101
+ @for (segment of segments(); track $index) {
102
+ <view [style]="segment.style"></view>
103
+ }
104
+ @for (dot of dots(); track $index) {
105
+ <view [style]="dot.style" (bindtap)="onPointTap(dot.point)"></view>
106
+ }
107
+ `,
108
+ })
109
+ export class UiLineSeries {
110
+ readonly #chart = inject(UiCartesianChart);
111
+
112
+ readonly data = input.required<ChartPoint[]>();
113
+ readonly color = input('var(--primary)');
114
+ readonly strokeWidth = input(2);
115
+ readonly dotRadius = input(3);
116
+ readonly showDots = input(true);
117
+ /** Draw a smooth (monotone-cubic) curve through the points instead of straight
118
+ * segments. Dots still sit on the raw data points. */
119
+ readonly smooth = input(false);
120
+ readonly userClass = input<string>('', { alias: 'class' });
121
+
122
+ readonly pointTap = output<ChartPoint>();
123
+
124
+ // Project each data point into plot-local pixel space via the shared scales.
125
+ // Reading the chart's scale signals here makes this recompute whenever the
126
+ // domain or plot size changes.
127
+ readonly #pixelPoints = computed(() => {
128
+ const scaleX = this.#chart.xScale();
129
+ const scaleY = this.#chart.yScale();
130
+ return this.data().map((point) => ({
131
+ x: scaleX(point.x),
132
+ y: scaleY(point.y),
133
+ point,
134
+ }));
135
+ });
136
+
137
+ protected readonly segments = computed(() => {
138
+ const stroke = this.strokeWidth();
139
+ const fill = this.color();
140
+ // Smooth mode replaces the raw polyline with a dense monotone-cubic
141
+ // resampling; `computeLineSegments` then rasterizes it identically — the
142
+ // curve is just made of many more, shorter rotated views.
143
+ const pixels = this.#pixelPoints();
144
+ const line = this.smooth() ? sampleSmoothLine(pixels) : pixels;
145
+ return computeLineSegments(line).map((s) => ({
146
+ style:
147
+ `position: absolute; left: ${px(s.x)}; top: ${px(s.y - stroke / 2)}; ` +
148
+ `width: ${px(s.length)}; height: ${px(stroke)}; ` +
149
+ `background-color: ${fill}; border-radius: ${px(stroke / 2)}; ` +
150
+ `transform-origin: 0 50%; transform: rotate(${s.angle.toFixed(2)}deg);`,
151
+ }));
152
+ });
153
+
154
+ protected readonly dots = computed(() => {
155
+ if (!this.showDots()) return [];
156
+ const radius = this.dotRadius();
157
+ const fill = this.color();
158
+ return this.#pixelPoints().map((p) => ({
159
+ point: p.point,
160
+ style:
161
+ `position: absolute; left: ${px(p.x - radius)}; top: ${px(p.y - radius)}; ` +
162
+ `width: ${px(radius * 2)}; height: ${px(radius * 2)}; ` +
163
+ `background-color: ${fill}; border-radius: ${px(radius)};`,
164
+ }));
165
+ });
166
+
167
+ protected onPointTap(point: ChartPoint): void {
168
+ this.pointTap.emit(point);
169
+ }
170
+ }
171
+
172
+ /**
173
+ * A ready-to-use line chart for the common single-series case. Composes a
174
+ * `<ui-cartesian-chart>` with one `<ui-line-series>` and auto-derives rounded
175
+ * axis domains + ticks from the data. For multiple series or full control over
176
+ * the axes, use `<ui-cartesian-chart>` with `<ui-line-series>` children directly.
177
+ */
178
+ @Component({
179
+ selector: 'ui-line-chart',
180
+ standalone: true,
181
+ imports: [LYNX_ELEMENTS, UiCartesianChart, UiLineSeries],
182
+ encapsulation: ViewEncapsulation.None,
183
+ template: `
184
+ <ui-cartesian-chart
185
+ [xDomain]="xAxis().domain"
186
+ [yDomain]="yAxis().domain"
187
+ [xTicks]="xAxis().ticks"
188
+ [yTicks]="yAxis().ticks"
189
+ [width]="width()"
190
+ [height]="height()"
191
+ [tickCount]="tickCount()"
192
+ [showXGrid]="showXGrid()"
193
+ [xAxisLabel]="xAxisLabel()"
194
+ [yAxisLabel]="yAxisLabel()"
195
+ [padding]="padding()"
196
+ [xTickFormat]="xTickFormat()"
197
+ [yTickFormat]="yTickFormat()"
198
+ [zoomable]="zoomable()"
199
+ [zoomAxes]="zoomAxes()"
200
+ [showZoomControls]="showZoomControls()"
201
+ [minZoom]="minZoom()"
202
+ [maxZoom]="maxZoom()"
203
+ [zoomStep]="zoomStep()"
204
+ [class]="userClass()"
205
+ >
206
+ <ui-line-series
207
+ [data]="data()"
208
+ [color]="color()"
209
+ [strokeWidth]="strokeWidth()"
210
+ [dotRadius]="dotRadius()"
211
+ [showDots]="showDots()"
212
+ [smooth]="smooth()"
213
+ (pointTap)="pointTap.emit($event)"
214
+ />
215
+ </ui-cartesian-chart>
216
+ `,
217
+ })
218
+ export class UiLineChart {
219
+ readonly data = input.required<ChartPoint[]>();
220
+ readonly color = input('var(--primary)');
221
+ readonly width = input(320);
222
+ readonly height = input(200);
223
+ readonly tickCount = input(5);
224
+ readonly strokeWidth = input(2);
225
+ readonly dotRadius = input(3);
226
+ readonly showDots = input(true);
227
+ /** Draw a smooth (monotone-cubic) curve through the points. */
228
+ readonly smooth = input(false);
229
+ /** Draw vertical gridlines at each x tick. */
230
+ readonly showXGrid = input(false);
231
+ /** Title for the x-axis (centered below the tick labels). */
232
+ readonly xAxisLabel = input<string>('');
233
+ /** Title for the y-axis (rotated in the left gutter). */
234
+ readonly yAxisLabel = input<string>('');
235
+ /** Inner inset of the plot area, each side a fraction (0–1) — see {@link ChartPadding}. */
236
+ readonly padding = input<ChartPadding>({});
237
+ /** Force the x-axis lower/upper bound instead of deriving it from the data —
238
+ * e.g. `xMin={0}` to anchor the origin. Omitted bounds fall back to the data. */
239
+ readonly xMin = input<number | undefined>(undefined);
240
+ readonly xMax = input<number | undefined>(undefined);
241
+ /** Force the y-axis lower/upper bound (e.g. `yMin={0}` to start at zero). */
242
+ readonly yMin = input<number | undefined>(undefined);
243
+ readonly yMax = input<number | undefined>(undefined);
244
+ readonly xTickFormat = input<(value: number) => string>(defaultTickFormat);
245
+ readonly yTickFormat = input<(value: number) => string>(defaultTickFormat);
246
+ readonly userClass = input<string>('', { alias: 'class' });
247
+
248
+ // Pan/zoom — forwarded to the underlying <ui-cartesian-chart>. Off by default.
249
+ readonly zoomable = input(false);
250
+ readonly zoomAxes = input<'x' | 'y' | 'xy'>('xy');
251
+ readonly showZoomControls = input(true);
252
+ readonly minZoom = input(1);
253
+ readonly maxZoom = input(8);
254
+ readonly zoomStep = input(1.4);
255
+
256
+ readonly pointTap = output<ChartPoint>();
257
+
258
+ // Auto-compute rounded axes from the data extremes, letting an explicit
259
+ // min/max override either end. `Math.min/max(...[])` yield ±Infinity for empty
260
+ // data, which `niceScale` handles by padding.
261
+ protected readonly xAxis = computed(() => {
262
+ const xs = this.data().map((p) => p.x);
263
+ const lo = this.xMin() ?? Math.min(...xs);
264
+ const hi = this.xMax() ?? Math.max(...xs);
265
+ return niceScale(lo, hi, this.tickCount());
266
+ });
267
+ protected readonly yAxis = computed(() => {
268
+ const ys = this.data().map((p) => p.y);
269
+ const lo = this.yMin() ?? Math.min(...ys);
270
+ const hi = this.yMax() ?? Math.max(...ys);
271
+ return niceScale(lo, hi, this.tickCount());
272
+ });
273
+ }
@@ -0,0 +1 @@
1
+ export { UiList } from './list';
@@ -0,0 +1,35 @@
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 { UiList } from './list';
7
+
8
+ @Component({
9
+ standalone: true,
10
+ imports: [UiList, LYNX_ELEMENTS],
11
+ template: `<ui-list><text>Item</text></ui-list>`,
12
+ })
13
+ class ListHost {}
14
+
15
+ describe('UiList', () => {
16
+ it('renders projected content', async () => {
17
+ // Lynx `<list>` is virtualized — items are only added to the native tree by
18
+ // the componentAtIndex callback, which isn't driven from a unit test. Just
19
+ // assert the wrapper mounts (its own template bindings + host bindings are
20
+ // covered) without asserting on the item's text.
21
+ const { container } = await render(ListHost);
22
+ expect(container).toBeTruthy();
23
+ });
24
+
25
+ it('emits an explicit height style when the height input is set', async () => {
26
+ // JIT template bindings don't propagate through input(); flip the signal
27
+ // directly so listStyle exercises its truthy branch.
28
+ const { container, componentRef } = await render(UiList);
29
+ setInputSignal((componentRef.instance as UiList).height, '400px');
30
+ await waitForUpdate();
31
+
32
+ const list = container.querySelector('list');
33
+ expect(list?.getAttribute('style')).toContain('height: 400px');
34
+ });
35
+ });
@@ -0,0 +1,71 @@
1
+ import {
2
+ Component,
3
+ ViewEncapsulation,
4
+ computed,
5
+ input,
6
+ output,
7
+ } from '@angular/core';
8
+ import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
9
+
10
+ import { cn } from '../../utils/cn';
11
+
12
+ @Component({
13
+ selector: 'ui-list',
14
+ standalone: true,
15
+ imports: [LYNX_ELEMENTS],
16
+ encapsulation: ViewEncapsulation.None,
17
+ // `|| undefined` on boolean attributes removes the attribute entirely when
18
+ // false. Passing the string "false" is treated as truthy by Lynx's native
19
+ // attribute parser, so absence is the only way to truly turn these off.
20
+ template: `
21
+ <list
22
+ [attr.list-type]="listType()"
23
+ [attr.span-count]="spanCount()"
24
+ [attr.scroll-orientation]="orientation()"
25
+ [attr.bounces]="bounces() || undefined"
26
+ [attr.scroll-bar-enable]="scrollBar() || undefined"
27
+ [attr.enable-nested-scroll]="nestedScroll() || undefined"
28
+ [attr.sticky]="sticky() || undefined"
29
+ [attr.sticky-offset]="stickyOffset()"
30
+ [attr.preload-buffer-count]="preloadBufferCount()"
31
+ [attr.list-main-axis-gap]="mainAxisGap()"
32
+ [attr.list-cross-axis-gap]="crossAxisGap()"
33
+ [attr.lower-threshold-item-count]="lowerThresholdItemCount()"
34
+ [attr.upper-threshold-item-count]="upperThresholdItemCount()"
35
+ [class]="listClass()"
36
+ [style]="listStyle()"
37
+ (bindscrolltoupper)="scrolledToStart.emit()"
38
+ (bindscrolltolower)="scrolledToEnd.emit()"
39
+ >
40
+ <ng-content />
41
+ </list>
42
+ `,
43
+ })
44
+ export class UiList {
45
+ readonly listType = input<'single' | 'flow' | 'waterfall'>('single');
46
+ readonly spanCount = input(1);
47
+ readonly orientation = input<'vertical' | 'horizontal'>('vertical');
48
+ readonly height = input<string>('');
49
+ readonly bounces = input(true);
50
+ readonly scrollBar = input(true);
51
+ readonly nestedScroll = input(false);
52
+ readonly sticky = input(false);
53
+ readonly stickyOffset = input(0);
54
+ readonly preloadBufferCount = input(0);
55
+ readonly mainAxisGap = input('');
56
+ readonly crossAxisGap = input('');
57
+ readonly lowerThresholdItemCount = input(0);
58
+ readonly upperThresholdItemCount = input(0);
59
+ readonly userClass = input<string>('', { alias: 'class' });
60
+
61
+ readonly scrolledToStart = output<void>();
62
+ readonly scrolledToEnd = output<void>();
63
+
64
+ protected readonly listClass = computed(() => cn('w-full', this.userClass()));
65
+
66
+ // Explicit height is required for list to scroll in Lynx
67
+ protected readonly listStyle = computed(() => {
68
+ const h = this.height();
69
+ return h ? `height: ${h};` : '';
70
+ });
71
+ }
@@ -0,0 +1,8 @@
1
+ export {
2
+ UiNavDrawer,
3
+ UiNavDrawerHeader,
4
+ UiNavDrawerContent,
5
+ UiNavDrawerItem,
6
+ UiNavDrawerFooter,
7
+ UiNavDrawerTrigger,
8
+ } from './nav-drawer';
@@ -0,0 +1,267 @@
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, vi } from 'vitest';
5
+ import { setInputSignal } from '../../../test-utils/set-input-signal';
6
+ import {
7
+ UiNavDrawer,
8
+ UiNavDrawerContent,
9
+ UiNavDrawerFooter,
10
+ UiNavDrawerHeader,
11
+ UiNavDrawerItem,
12
+ UiNavDrawerTrigger,
13
+ } from './nav-drawer';
14
+
15
+ @Component({
16
+ standalone: true,
17
+ imports: [
18
+ UiNavDrawer,
19
+ UiNavDrawerHeader,
20
+ UiNavDrawerContent,
21
+ UiNavDrawerItem,
22
+ UiNavDrawerFooter,
23
+ UiNavDrawerTrigger,
24
+ LYNX_ELEMENTS,
25
+ ],
26
+ template: `
27
+ <ui-nav-drawer [(open)]="open">
28
+ <ui-nav-drawer-header><text>Nav</text></ui-nav-drawer-header>
29
+ <ui-nav-drawer-content>
30
+ <ui-nav-drawer-item>Home</ui-nav-drawer-item>
31
+ </ui-nav-drawer-content>
32
+ <ui-nav-drawer-footer><text>Foot</text></ui-nav-drawer-footer>
33
+ </ui-nav-drawer>
34
+ <ui-nav-drawer-trigger>Open</ui-nav-drawer-trigger>
35
+ `,
36
+ })
37
+ class NavDrawerHost {
38
+ open = true;
39
+ }
40
+
41
+ describe('UiNavDrawer', () => {
42
+ const patchRefs = (inst: UiNavDrawer, view: Element) => {
43
+ (inst as unknown as { backdropRef: unknown }).backdropRef = signal({
44
+ nativeElement: view,
45
+ });
46
+ (inst as unknown as { panelRef: unknown }).panelRef = signal({
47
+ nativeElement: view,
48
+ });
49
+ };
50
+
51
+ it('renders header, content items, footer, and trigger', async () => {
52
+ const { container } = await render(NavDrawerHost);
53
+ expect(container).toBeTruthy();
54
+ });
55
+
56
+ it('backdrop tap closes the drawer', async () => {
57
+ const { componentRef } = await render(UiNavDrawer);
58
+ const inst = componentRef.instance as UiNavDrawer;
59
+ inst.open.set(true);
60
+ await waitForUpdate();
61
+ (inst as unknown as { onBackdropTap: () => void }).onBackdropTap();
62
+ expect(inst.open()).toBe(false);
63
+ });
64
+
65
+ it('close() sets open to false', async () => {
66
+ const { componentRef } = await render(UiNavDrawer);
67
+ const inst = componentRef.instance as UiNavDrawer;
68
+ inst.open.set(true);
69
+ inst.close();
70
+ expect(inst.open()).toBe(false);
71
+ });
72
+
73
+ it('panel tap is a no-op', async () => {
74
+ const { componentRef } = await render(UiNavDrawer);
75
+ const inst = componentRef.instance as UiNavDrawer;
76
+ expect(() =>
77
+ (inst as unknown as { onPanelTap: () => void }).onPanelTap(),
78
+ ).not.toThrow();
79
+ });
80
+
81
+ it('open effect runs the animate-in and animate-out lifecycles (left side)', async () => {
82
+ const { componentRef, container } = await render(UiNavDrawer);
83
+ const inst = componentRef.instance as UiNavDrawer;
84
+ const view = container.querySelector('view') as Element;
85
+ patchRefs(inst, view);
86
+ inst.open.set(true);
87
+ await waitForUpdate();
88
+ await waitForUpdate();
89
+ await waitForUpdate();
90
+
91
+ inst.open.set(false);
92
+ await new Promise((r) => setTimeout(r, 320));
93
+ await waitForUpdate();
94
+ expect(inst.open()).toBe(false);
95
+ });
96
+
97
+ it('open effect runs correctly for the right side', async () => {
98
+ const { componentRef, container } = await render(UiNavDrawer);
99
+ const inst = componentRef.instance as UiNavDrawer;
100
+ setInputSignal(inst.side, 'right');
101
+ await waitForUpdate();
102
+ const view = container.querySelector('view') as Element;
103
+ patchRefs(inst, view);
104
+ inst.open.set(true);
105
+ await waitForUpdate();
106
+ await waitForUpdate();
107
+ await waitForUpdate();
108
+ inst.open.set(false);
109
+ await new Promise((r) => setTimeout(r, 320));
110
+ expect(inst.open()).toBe(false);
111
+ expect(
112
+ Array.from(container.querySelectorAll('view')).some((el) =>
113
+ el.getAttribute('class')?.includes('border-l'),
114
+ ),
115
+ ).toBe(true);
116
+ });
117
+
118
+ it('open effect bails when backdrop/panel refs are undefined', async () => {
119
+ const { componentRef } = await render(UiNavDrawer);
120
+ const inst = componentRef.instance as UiNavDrawer;
121
+ inst.open.set(true);
122
+ await waitForUpdate();
123
+ await waitForUpdate();
124
+ inst.open.set(false);
125
+ await waitForUpdate();
126
+ expect(inst.open()).toBe(false);
127
+ });
128
+ });
129
+
130
+ describe('UiNavDrawerHeader / Footer', () => {
131
+ it('render projected content', async () => {
132
+ const header = await render(UiNavDrawerHeader);
133
+ const footer = await render(UiNavDrawerFooter);
134
+ expect(header.container.querySelector('view')).toBeTruthy();
135
+ expect(footer.container.querySelector('view')).toBeTruthy();
136
+ });
137
+ });
138
+
139
+ describe('UiNavDrawerContent', () => {
140
+ it('renders the scroll-view', async () => {
141
+ const parentResult = await render(UiNavDrawer);
142
+ const parent = parentResult.componentRef.instance as UiNavDrawer;
143
+ const contentResult = await render(UiNavDrawerContent, {
144
+ providers: [{ provide: UiNavDrawer, useValue: parent }],
145
+ });
146
+ expect(contentResult.container.querySelector('scroll-view')).toBeTruthy();
147
+ });
148
+
149
+ it('onContentLayout scrolls active item into view when drawer is open', async () => {
150
+ const parentResult = await render(UiNavDrawer);
151
+ const parent = parentResult.componentRef.instance as UiNavDrawer;
152
+ parent.open.set(true);
153
+ await waitForUpdate();
154
+
155
+ const contentResult = await render(UiNavDrawerContent, {
156
+ providers: [{ provide: UiNavDrawer, useValue: parent }],
157
+ });
158
+ const content = contentResult.componentRef.instance as UiNavDrawerContent;
159
+ (content as unknown as { onContentLayout: () => void }).onContentLayout();
160
+ expect(parent.open()).toBe(true);
161
+ });
162
+
163
+ it('onContentLayout is a no-op when drawer is closed', async () => {
164
+ const parentResult = await render(UiNavDrawer);
165
+ const parent = parentResult.componentRef.instance as UiNavDrawer;
166
+ const contentResult = await render(UiNavDrawerContent, {
167
+ providers: [{ provide: UiNavDrawer, useValue: parent }],
168
+ });
169
+ const content = contentResult.componentRef.instance as UiNavDrawerContent;
170
+ expect(() =>
171
+ (content as unknown as { onContentLayout: () => void }).onContentLayout(),
172
+ ).not.toThrow();
173
+ });
174
+ });
175
+
176
+ describe('UiNavDrawerItem', () => {
177
+ const renderItem = async (autoClose = true) => {
178
+ const parentResult = await render(UiNavDrawer);
179
+ const parent = parentResult.componentRef.instance as UiNavDrawer;
180
+ parent.open.set(true);
181
+ await waitForUpdate();
182
+
183
+ const itemResult = await render(UiNavDrawerItem, {
184
+ providers: [{ provide: UiNavDrawer, useValue: parent }],
185
+ });
186
+ const item = itemResult.componentRef.instance as UiNavDrawerItem;
187
+ setInputSignal(item.autoClose, autoClose);
188
+ await waitForUpdate();
189
+ return { parent, item, itemResult };
190
+ };
191
+
192
+ it('onTap emits pressed and auto-closes the drawer by default', async () => {
193
+ const { parent, item } = await renderItem();
194
+ const emitted = vi.fn();
195
+ item.pressed.subscribe(emitted);
196
+ (item as unknown as { onTap: () => void }).onTap();
197
+ expect(emitted).toHaveBeenCalledTimes(1);
198
+ expect(parent.open()).toBe(false);
199
+ });
200
+
201
+ it('onTap does not auto-close when autoClose is false', async () => {
202
+ const { parent, item } = await renderItem(false);
203
+ (item as unknown as { onTap: () => void }).onTap();
204
+ expect(parent.open()).toBe(true);
205
+ });
206
+
207
+ it('press handlers run cleanly', async () => {
208
+ const { item } = await renderItem();
209
+ const inst = item as unknown as {
210
+ onPressStart: () => void;
211
+ onPressEnd: () => void;
212
+ onPressCancel: () => void;
213
+ };
214
+ expect(() => inst.onPressStart()).not.toThrow();
215
+ expect(() => inst.onPressEnd()).not.toThrow();
216
+ expect(() => inst.onPressCancel()).not.toThrow();
217
+ });
218
+
219
+ it('active item carries the accent background', async () => {
220
+ const { item, itemResult } = await renderItem();
221
+ setInputSignal(item.active, true);
222
+ await waitForUpdate();
223
+ const view = itemResult.container.querySelector('view');
224
+ expect(view?.getAttribute('class')).toContain('bg-accent');
225
+ });
226
+
227
+ it('scrollIntoView calls invoke() on the container ref when populated', async () => {
228
+ const { item, itemResult } = await renderItem();
229
+ const invoke = vi.fn();
230
+ (item as unknown as { containerRef: unknown }).containerRef = signal({
231
+ nativeElement: { invoke },
232
+ });
233
+ item.scrollIntoView();
234
+ expect(invoke).toHaveBeenCalledWith('scrollIntoView', expect.any(Object));
235
+
236
+ // No-container path: containerRef is undefined so `?.` short-circuits.
237
+ (item as unknown as { containerRef: () => undefined }).containerRef = () =>
238
+ undefined;
239
+ expect(() => item.scrollIntoView()).not.toThrow();
240
+ void itemResult;
241
+ });
242
+ });
243
+
244
+ describe('UiNavDrawerTrigger', () => {
245
+ it('onTap emits pressed', async () => {
246
+ const { componentRef } = await render(UiNavDrawerTrigger);
247
+ const inst = componentRef.instance as unknown as UiNavDrawerTrigger & {
248
+ onTap: () => void;
249
+ };
250
+ const emitted = vi.fn();
251
+ inst.pressed.subscribe(emitted);
252
+ inst.onTap();
253
+ expect(emitted).toHaveBeenCalledTimes(1);
254
+ });
255
+
256
+ it('press handlers run cleanly', async () => {
257
+ const { componentRef } = await render(UiNavDrawerTrigger);
258
+ const inst = componentRef.instance as unknown as {
259
+ onPressStart: () => void;
260
+ onPressEnd: () => void;
261
+ onPressCancel: () => void;
262
+ };
263
+ expect(() => inst.onPressStart()).not.toThrow();
264
+ expect(() => inst.onPressEnd()).not.toThrow();
265
+ expect(() => inst.onPressCancel()).not.toThrow();
266
+ });
267
+ });