@blotch/dolan 22.2.2 → 22.2.4

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 (259) hide show
  1. package/dist/commands/add.d.ts +6 -2
  2. package/dist/commands/add.js +41 -11
  3. package/dist/commands/add.js.map +1 -1
  4. package/dist/commands/add.test.js +162 -9
  5. package/dist/commands/add.test.js.map +1 -1
  6. package/dist/commands/diff.d.ts +2 -1
  7. package/dist/commands/diff.js +38 -25
  8. package/dist/commands/diff.js.map +1 -1
  9. package/dist/commands/diff.test.js +72 -11
  10. package/dist/commands/diff.test.js.map +1 -1
  11. package/dist/commands/doctor.js +228 -34
  12. package/dist/commands/doctor.js.map +1 -1
  13. package/dist/commands/doctor.test.js +367 -10
  14. package/dist/commands/doctor.test.js.map +1 -1
  15. package/dist/commands/eject.js +7 -0
  16. package/dist/commands/eject.js.map +1 -1
  17. package/dist/commands/eject.test.js +39 -0
  18. package/dist/commands/eject.test.js.map +1 -1
  19. package/dist/commands/info.js +3 -2
  20. package/dist/commands/info.js.map +1 -1
  21. package/dist/commands/init.d.ts +37 -1
  22. package/dist/commands/init.js +169 -40
  23. package/dist/commands/init.js.map +1 -1
  24. package/dist/commands/init.test.js +392 -27
  25. package/dist/commands/init.test.js.map +1 -1
  26. package/dist/commands/install-packages.d.ts +6 -0
  27. package/dist/commands/install-packages.js +20 -0
  28. package/dist/commands/install-packages.js.map +1 -0
  29. package/dist/commands/install-packages.test.d.ts +1 -0
  30. package/dist/commands/install-packages.test.js +55 -0
  31. package/dist/commands/install-packages.test.js.map +1 -0
  32. package/dist/commands/list.js +21 -14
  33. package/dist/commands/list.js.map +1 -1
  34. package/dist/commands/list.test.js +62 -5
  35. package/dist/commands/list.test.js.map +1 -1
  36. package/dist/commands/outdated.js +11 -10
  37. package/dist/commands/outdated.js.map +1 -1
  38. package/dist/commands/outdated.test.js +20 -5
  39. package/dist/commands/outdated.test.js.map +1 -1
  40. package/dist/commands/remove.d.ts +4 -0
  41. package/dist/commands/remove.js +17 -4
  42. package/dist/commands/remove.js.map +1 -1
  43. package/dist/commands/remove.test.js +53 -2
  44. package/dist/commands/remove.test.js.map +1 -1
  45. package/dist/commands/theme.js +1 -1
  46. package/dist/commands/theme.js.map +1 -1
  47. package/dist/commands/update.d.ts +11 -0
  48. package/dist/commands/update.js +174 -58
  49. package/dist/commands/update.js.map +1 -1
  50. package/dist/commands/update.test.js +587 -33
  51. package/dist/commands/update.test.js.map +1 -1
  52. package/dist/index.js +7 -0
  53. package/dist/index.js.map +1 -1
  54. package/dist/lockfile.d.ts +7 -0
  55. package/dist/lockfile.js +36 -0
  56. package/dist/lockfile.js.map +1 -1
  57. package/dist/lockfile.test.d.ts +1 -0
  58. package/dist/lockfile.test.js +25 -0
  59. package/dist/lockfile.test.js.map +1 -0
  60. package/dist/registry.d.ts +14 -0
  61. package/dist/registry.js +78 -24
  62. package/dist/registry.js.map +1 -1
  63. package/dist/registry.test.js +119 -38
  64. package/dist/registry.test.js.map +1 -1
  65. package/dist/test-utils.d.ts +5 -0
  66. package/dist/test-utils.js +6 -1
  67. package/dist/test-utils.js.map +1 -1
  68. package/dist/testing/fire-lynx-event.d.ts +14 -0
  69. package/dist/testing/fire-lynx-event.js +19 -0
  70. package/dist/testing/fire-lynx-event.js.map +1 -0
  71. package/dist/testing/fire-lynx-event.test.d.ts +1 -0
  72. package/dist/testing/fire-lynx-event.test.js +44 -0
  73. package/dist/testing/fire-lynx-event.test.js.map +1 -0
  74. package/dist/testing/lynx-main-thread.d.ts +22 -0
  75. package/dist/testing/lynx-main-thread.js +11 -0
  76. package/dist/testing/lynx-main-thread.js.map +1 -0
  77. package/dist/testing/lynx-main-thread.test.d.ts +1 -0
  78. package/dist/testing/lynx-main-thread.test.js +24 -0
  79. package/dist/testing/lynx-main-thread.test.js.map +1 -0
  80. package/dist/testing/set-input-signal.d.ts +14 -0
  81. package/dist/testing/set-input-signal.js +21 -0
  82. package/dist/testing/set-input-signal.js.map +1 -0
  83. package/dist/testing/set-input-signal.test.d.ts +1 -0
  84. package/dist/testing/set-input-signal.test.js +20 -0
  85. package/dist/testing/set-input-signal.test.js.map +1 -0
  86. package/dist/ui/components/accordion/accordion.test.ts +158 -144
  87. package/dist/ui/components/accordion/accordion.ts +19 -14
  88. package/dist/ui/components/action-sheet/action-sheet.test.ts +283 -151
  89. package/dist/ui/components/action-sheet/action-sheet.ts +77 -43
  90. package/dist/ui/components/alert/alert.test.ts +67 -19
  91. package/dist/ui/components/alert/alert.ts +14 -16
  92. package/dist/ui/components/alert-dialog/alert-dialog.test.ts +208 -94
  93. package/dist/ui/components/alert-dialog/alert-dialog.ts +75 -38
  94. package/dist/ui/components/area-chart/area-chart.test.ts +134 -119
  95. package/dist/ui/components/area-chart/area-chart.ts +100 -315
  96. package/dist/ui/components/area-chart/index.ts +2 -3
  97. package/dist/ui/components/avatar/avatar.test.ts +89 -45
  98. package/dist/ui/components/badge/badge.test.ts +57 -23
  99. package/dist/ui/components/badge/badge.ts +12 -14
  100. package/dist/ui/components/bar-chart/bar-chart.test.ts +172 -45
  101. package/dist/ui/components/bar-chart/bar-chart.ts +140 -294
  102. package/dist/ui/components/bar-chart/index.ts +3 -3
  103. package/dist/ui/components/bottom-sheet/bottom-sheet.test.ts +386 -128
  104. package/dist/ui/components/bottom-sheet/bottom-sheet.ts +170 -53
  105. package/dist/ui/components/button/button.test.ts +611 -2
  106. package/dist/ui/components/button/button.ts +434 -49
  107. package/dist/ui/components/button/index.ts +2 -0
  108. package/dist/ui/components/button-group/button-group.test.ts +484 -2
  109. package/dist/ui/components/button-group/button-group.ts +380 -68
  110. package/dist/ui/components/button-group/index.ts +3 -0
  111. package/dist/ui/components/candlestick-chart/candlestick-chart.test.ts +183 -58
  112. package/dist/ui/components/candlestick-chart/candlestick-chart.ts +150 -323
  113. package/dist/ui/components/candlestick-chart/index.ts +3 -4
  114. package/dist/ui/components/card/card.test.ts +82 -62
  115. package/dist/ui/components/chart/chart-focus.test.ts +132 -0
  116. package/dist/ui/components/chart/chart-focus.ts +115 -0
  117. package/dist/ui/components/chart/chart-presets.test.ts +335 -0
  118. package/dist/ui/components/chart/chart-presets.ts +278 -0
  119. package/dist/ui/components/chart/chart-scene.test.ts +589 -0
  120. package/dist/ui/components/chart/chart-scene.ts +483 -0
  121. package/dist/ui/components/chart/chart-theme.test.ts +85 -0
  122. package/dist/ui/components/chart/chart-theme.ts +210 -0
  123. package/dist/ui/components/chart/chart-zoom.test.ts +159 -0
  124. package/dist/ui/components/chart/chart-zoom.ts +162 -0
  125. package/dist/ui/components/chart/chart.test.ts +1017 -0
  126. package/dist/ui/components/chart/chart.ts +934 -0
  127. package/dist/ui/components/chart/index.ts +24 -0
  128. package/dist/ui/components/checkbox/checkbox.test.ts +124 -42
  129. package/dist/ui/components/checkbox/checkbox.ts +23 -23
  130. package/dist/ui/components/collapsible/collapsible.test.ts +158 -81
  131. package/dist/ui/components/collapsible/collapsible.ts +19 -14
  132. package/dist/ui/components/context-menu/context-menu.test.ts +224 -0
  133. package/dist/ui/components/context-menu/context-menu.ts +110 -0
  134. package/dist/ui/components/context-menu/index.ts +1 -0
  135. package/dist/ui/components/dialog/dialog.test.ts +213 -108
  136. package/dist/ui/components/dialog/dialog.ts +72 -35
  137. package/dist/ui/components/dock/dock-scroll.test.ts +130 -0
  138. package/dist/ui/components/dock/dock-scroll.ts +133 -0
  139. package/dist/ui/components/dock/dock.test.ts +749 -0
  140. package/dist/ui/components/dock/dock.ts +960 -0
  141. package/dist/ui/components/dock/index.ts +2 -0
  142. package/dist/ui/components/empty-state/empty-state.test.ts +28 -39
  143. package/dist/ui/components/glass/concentric-radius.test.ts +13 -0
  144. package/dist/ui/components/glass/concentric-radius.ts +17 -0
  145. package/dist/ui/components/glass/glass-design.test.ts +78 -0
  146. package/dist/ui/components/glass/glass-design.ts +119 -0
  147. package/dist/ui/components/glass/glass.test.ts +338 -0
  148. package/dist/ui/components/glass/glass.ts +360 -0
  149. package/dist/ui/components/glass/index.ts +18 -0
  150. package/dist/ui/components/icon/icon-theme.test.ts +169 -0
  151. package/dist/ui/components/icon/icon-theme.ts +227 -0
  152. package/dist/ui/components/icon/icon.test.ts +85 -25
  153. package/dist/ui/components/icon/icon.ts +38 -21
  154. package/dist/ui/components/input/input.test.ts +178 -101
  155. package/dist/ui/components/input/input.ts +15 -12
  156. package/dist/ui/components/label/label.test.ts +11 -24
  157. package/dist/ui/components/line-chart/index.ts +2 -3
  158. package/dist/ui/components/line-chart/line-chart.test.ts +149 -56
  159. package/dist/ui/components/line-chart/line-chart.ts +83 -241
  160. package/dist/ui/components/list/list.test.ts +119 -21
  161. package/dist/ui/components/list/list.ts +21 -15
  162. package/dist/ui/components/menu/index.ts +25 -0
  163. package/dist/ui/components/menu/menu-placement.test.ts +153 -0
  164. package/dist/ui/components/menu/menu-placement.ts +159 -0
  165. package/dist/ui/components/menu/menu-popup.test.ts +632 -0
  166. package/dist/ui/components/menu/menu-popup.ts +544 -0
  167. package/dist/ui/components/menu/menu.test.ts +429 -0
  168. package/dist/ui/components/menu/menu.ts +415 -0
  169. package/dist/ui/components/nav-drawer/nav-drawer.test.ts +435 -191
  170. package/dist/ui/components/nav-drawer/nav-drawer.ts +127 -56
  171. package/dist/ui/components/pagination/pagination.test.ts +177 -65
  172. package/dist/ui/components/pagination/pagination.ts +55 -8
  173. package/dist/ui/components/progress/progress.test.ts +102 -46
  174. package/dist/ui/components/progress/progress.ts +52 -54
  175. package/dist/ui/components/radio-group/radio-group.test.ts +128 -83
  176. package/dist/ui/components/radio-group/radio-group.ts +24 -24
  177. package/dist/ui/components/scatter-chart/index.ts +2 -3
  178. package/dist/ui/components/scatter-chart/scatter-chart.test.ts +131 -26
  179. package/dist/ui/components/scatter-chart/scatter-chart.ts +104 -250
  180. package/dist/ui/components/scroll-area/scroll-area.test.ts +95 -24
  181. package/dist/ui/components/scroll-area/scroll-area.ts +8 -8
  182. package/dist/ui/components/select/select.test.ts +138 -78
  183. package/dist/ui/components/select/select.ts +17 -11
  184. package/dist/ui/components/separator/separator.test.ts +18 -17
  185. package/dist/ui/components/sheet/sheet.test.ts +64 -11
  186. package/dist/ui/components/sheet/sheet.ts +9 -1
  187. package/dist/ui/components/skeleton/skeleton.test.ts +24 -7
  188. package/dist/ui/components/skeleton/skeleton.ts +10 -16
  189. package/dist/ui/components/spinner/spinner.test.ts +67 -22
  190. package/dist/ui/components/spinner/spinner.ts +48 -41
  191. package/dist/ui/components/switch/switch.test.ts +116 -46
  192. package/dist/ui/components/switch/switch.ts +8 -13
  193. package/dist/ui/components/tab-bar/index.ts +1 -0
  194. package/dist/ui/components/tab-bar/tab-bar.test.ts +280 -0
  195. package/dist/ui/components/tab-bar/tab-bar.ts +230 -0
  196. package/dist/ui/components/tabs/tabs.test.ts +215 -157
  197. package/dist/ui/components/tabs/tabs.ts +110 -118
  198. package/dist/ui/components/textarea/textarea.test.ts +206 -100
  199. package/dist/ui/components/textarea/textarea.ts +14 -12
  200. package/dist/ui/components/toast/toast.test.ts +436 -315
  201. package/dist/ui/components/toast/toast.ts +62 -55
  202. package/dist/ui/components/toggle/toggle.test.ts +247 -140
  203. package/dist/ui/theme/chart-colors.test.ts +35 -0
  204. package/dist/ui/theme/dark.css +46 -2
  205. package/dist/ui/theme/default.css +63 -7
  206. package/dist/ui/theme/icon-colors.test.ts +35 -0
  207. package/dist/ui/theme/rgb-channels.test.ts +84 -0
  208. package/dist/ui/theme/tailwind-build.test.ts +114 -0
  209. package/dist/ui/theme/tailwind-css.test.ts +83 -0
  210. package/dist/ui/theme/tailwind-plugin.test.ts +42 -2
  211. package/dist/ui/theme/tailwind-plugin.ts +98 -25
  212. package/dist/ui/theme/tailwind.css +113 -0
  213. package/dist/ui/utils/animate.test.ts +13 -1
  214. package/dist/utils/analyze.d.ts +14 -0
  215. package/dist/utils/analyze.js +15 -0
  216. package/dist/utils/analyze.js.map +1 -1
  217. package/dist/utils/analyze.test.js +14 -1
  218. package/dist/utils/analyze.test.js.map +1 -1
  219. package/dist/utils/installed.d.ts +29 -0
  220. package/dist/utils/installed.js +43 -0
  221. package/dist/utils/installed.js.map +1 -0
  222. package/dist/utils/installed.test.d.ts +1 -0
  223. package/dist/utils/installed.test.js +57 -0
  224. package/dist/utils/installed.test.js.map +1 -0
  225. package/dist/utils/interactive.d.ts +9 -0
  226. package/dist/utils/interactive.js +10 -0
  227. package/dist/utils/interactive.js.map +1 -0
  228. package/dist/utils/interactive.test.d.ts +1 -0
  229. package/dist/utils/interactive.test.js +24 -0
  230. package/dist/utils/interactive.test.js.map +1 -0
  231. package/dist/utils/packages.d.ts +36 -0
  232. package/dist/utils/packages.js +70 -0
  233. package/dist/utils/packages.js.map +1 -0
  234. package/dist/utils/packages.test.d.ts +1 -0
  235. package/dist/utils/packages.test.js +89 -0
  236. package/dist/utils/packages.test.js.map +1 -0
  237. package/dist/utils/resolve-paths.d.ts +17 -1
  238. package/dist/utils/resolve-paths.js +21 -3
  239. package/dist/utils/resolve-paths.js.map +1 -1
  240. package/dist/utils/resolve-paths.test.js +38 -2
  241. package/dist/utils/resolve-paths.test.js.map +1 -1
  242. package/dist/utils/tailwind.d.ts +25 -0
  243. package/dist/utils/tailwind.js +51 -0
  244. package/dist/utils/tailwind.js.map +1 -0
  245. package/dist/utils/tailwind.test.d.ts +1 -0
  246. package/dist/utils/tailwind.test.js +71 -0
  247. package/dist/utils/tailwind.test.js.map +1 -0
  248. package/package.json +13 -1
  249. package/dist/ui/components/area-chart/area-chart-render.test.ts +0 -121
  250. package/dist/ui/components/bar-chart/bar-chart-render.test.ts +0 -174
  251. package/dist/ui/components/button/button-render.test.ts +0 -113
  252. package/dist/ui/components/button-group/button-group-render.test.ts +0 -146
  253. package/dist/ui/components/candlestick-chart/candlestick-chart-render.test.ts +0 -174
  254. package/dist/ui/components/cartesian-chart/cartesian-chart-render.test.ts +0 -188
  255. package/dist/ui/components/cartesian-chart/cartesian-chart.test.ts +0 -525
  256. package/dist/ui/components/cartesian-chart/cartesian-chart.ts +0 -1140
  257. package/dist/ui/components/cartesian-chart/index.ts +0 -18
  258. package/dist/ui/components/line-chart/line-chart-render.test.ts +0 -121
  259. package/dist/ui/components/scatter-chart/scatter-chart-render.test.ts +0 -157
@@ -2,272 +2,114 @@ import {
2
2
  Component,
3
3
  ViewEncapsulation,
4
4
  computed,
5
- inject,
6
5
  input,
7
6
  output,
8
7
  } from '@angular/core';
9
- import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
8
+ import { d3Curve, defineChart, dot, lineY } from '@tanstack/charts/universal';
9
+ import type { ChartMark, ChartPoint } from '@tanstack/charts/types';
10
+ import { curveMonotoneX } from 'd3-shape';
10
11
 
12
+ import { UiChart, tooltip } from '../chart/chart';
11
13
  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
- };
14
+ type CartesianPresetOptions,
15
+ PRESET_CHART_BINDINGS,
16
+ UiCartesianPreset,
17
+ type XYDatum,
18
+ presetAxes,
19
+ } from '../chart/chart-presets';
20
+
21
+ export type LineChartOptions = {
22
+ readonly data: readonly XYDatum[];
23
+ /** Any CSS color; theme tokens like `var(--primary)` follow dark mode. */
24
+ readonly color?: string;
25
+ readonly strokeWidth?: number;
26
+ readonly dotRadius?: number;
27
+ readonly showDots?: boolean;
28
+ /** Draws a monotone curve through the points instead of straight segments. */
29
+ readonly smooth?: boolean;
30
+ } & CartesianPresetOptions;
31
31
 
32
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`).
33
+ * The definition `<ui-line-chart>` renders, for when you want to start from
34
+ * the preset and add marks of your own:
36
35
  *
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.
36
+ * ```ts
37
+ * const base = lineChartDefinition({ data });
38
+ * const withTarget = defineChart({ ...base, marks: [...base.marks, ruleY([80])] });
39
+ * ```
42
40
  */
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 });
41
+ export const lineChartDefinition = (options: LineChartOptions) => {
42
+ const {
43
+ data,
44
+ color = 'var(--primary)',
45
+ strokeWidth = 2,
46
+ dotRadius = 3,
47
+ showDots = true,
48
+ smooth = false,
49
+ } = options;
50
+ const curve = smooth ? d3Curve(curveMonotoneX) : undefined;
51
+ const marks: ChartMark<XYDatum, any, any, any, any, any, any>[] = [
52
+ lineY(data, { x: 'x', y: 'y', stroke: color, strokeWidth, curve }),
53
+ ];
54
+ if (showDots && dotRadius > 0) {
55
+ marks.push(dot(data, { x: 'x', y: 'y', r: dotRadius, fill: color }));
56
56
  }
57
- return segments;
57
+ return defineChart({
58
+ marks,
59
+ scales: presetAxes(options, {
60
+ x: data.map((d) => d.x),
61
+ y: data.map((d) => d.y),
62
+ }),
63
+ tooltip: options.tooltip === false ? false : tooltip,
64
+ });
58
65
  };
59
66
 
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
67
  /**
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.
68
+ * A single-series line chart. For several series, a reference line or any
69
+ * other mark, build a definition and render it with `<ui-chart>`, starting
70
+ * from {@link lineChartDefinition} if you like.
80
71
  *
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.
72
+ * ```html
73
+ * <ui-line-chart [data]="revenue" [smooth]="true" yAxisLabel="Revenue" />
74
+ * ```
177
75
  */
178
76
  @Component({
179
77
  selector: 'ui-line-chart',
180
- standalone: true,
181
- imports: [LYNX_ELEMENTS, UiCartesianChart, UiLineSeries],
78
+ imports: [UiChart],
182
79
  encapsulation: ViewEncapsulation.None,
183
80
  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>
81
+ <ui-chart
82
+ [definition]="definition()"
83
+ ${PRESET_CHART_BINDINGS}
84
+ (pointSelect)="onSelect($event)"
85
+ />
216
86
  `,
217
87
  })
218
- export class UiLineChart {
219
- readonly data = input.required<ChartPoint[]>();
88
+ export class UiLineChart extends UiCartesianPreset<XYDatum> {
89
+ readonly data = input.required<readonly XYDatum[]>();
220
90
  readonly color = input('var(--primary)');
221
- readonly width = input(320);
222
- readonly height = input(200);
223
- readonly tickCount = input(5);
224
91
  readonly strokeWidth = input(2);
225
92
  readonly dotRadius = input(3);
226
93
  readonly showDots = input(true);
227
- /** Draw a smooth (monotone-cubic) curve through the points. */
228
94
  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
95
 
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
- });
96
+ /** The tapped point's datum. */
97
+ readonly pointTap = output<XYDatum>();
98
+
99
+ protected readonly definition = computed(() =>
100
+ lineChartDefinition({
101
+ ...this.commonOptions(),
102
+ data: this.data(),
103
+ color: this.color(),
104
+ strokeWidth: this.strokeWidth(),
105
+ dotRadius: this.dotRadius(),
106
+ showDots: this.showDots(),
107
+ smooth: this.smooth(),
108
+ }),
109
+ );
110
+
111
+ protected onSelect(point: ChartPoint | null): void {
112
+ const datum = this.datumOf(point);
113
+ if (datum) this.pointTap.emit(datum);
114
+ }
273
115
  }
@@ -1,35 +1,133 @@
1
- import { Component } from '@angular/core';
2
- import { render, waitForUpdate } from '@blotch/angular-lynx-testing-library';
1
+ import { Component, signal } from '@angular/core';
2
+ import {
3
+ fireEvent,
4
+ render,
5
+ waitForUpdate,
6
+ } from '@blotch/angular-lynx-testing-library';
3
7
  import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
4
- import { describe, expect, it } from 'vitest';
5
- import { setInputSignal } from '../../../test-utils/set-input-signal';
8
+ import { describe, expect, it, vi } from 'vitest';
9
+ import { lynxMainThread } from '../../../test-utils/lynx-main-thread';
6
10
  import { UiList } from './list';
7
11
 
8
12
  @Component({
9
- standalone: true,
10
13
  imports: [UiList, LYNX_ELEMENTS],
11
- template: `<ui-list><text>Item</text></ui-list>`,
14
+ template: `
15
+ <ui-list
16
+ [height]="height()"
17
+ [mainAxisGap]="mainAxisGap()"
18
+ [crossAxisGap]="crossAxisGap()"
19
+ [bounces]="bounces()"
20
+ [scrollBar]="scrollBar()"
21
+ [nestedScroll]="nestedScroll()"
22
+ [sticky]="sticky()"
23
+ class="bg-card"
24
+ (scrolledToStart)="reached.push('start')"
25
+ (scrolledToEnd)="reached.push('end')"
26
+ >
27
+ <text>Item</text>
28
+ </ui-list>
29
+ `,
12
30
  })
13
- class ListHost {}
31
+ class ListHost {
32
+ readonly height = signal('');
33
+ readonly mainAxisGap = signal('');
34
+ readonly crossAxisGap = signal('');
35
+ readonly bounces = signal(true);
36
+ readonly scrollBar = signal(true);
37
+ readonly nestedScroll = signal(false);
38
+ readonly sticky = signal(false);
39
+ readonly reached: string[] = [];
40
+ }
41
+
42
+ const renderList = async () => {
43
+ const result = await render(ListHost);
44
+ return {
45
+ ...result,
46
+ host: result.componentRef.instance as ListHost,
47
+ list: () => result.container.querySelector('list')!,
48
+ };
49
+ };
50
+
51
+ /**
52
+ * The values each boolean reached Lynx with, through `__SetAttribute`.
53
+ */
54
+ const watchAttributes = () => {
55
+ const setAttribute = vi.spyOn(lynxMainThread(), '__SetAttribute');
56
+ return {
57
+ last: (name: string): unknown =>
58
+ setAttribute.mock.calls.filter((call) => call[1] === name).at(-1)?.[2],
59
+ restore: () => setAttribute.mockRestore(),
60
+ };
61
+ };
14
62
 
15
63
  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();
64
+ it('renders a single-column vertical list with the given class', async () => {
65
+ // Lynx `<list>` is virtualized: items join the native tree only through
66
+ // its componentAtIndex callback, which a unit test doesn't drive. So this
67
+ // checks the list itself rather than the projected item.
68
+ const { list } = await renderList();
69
+ expect(list().getAttribute('list-type')).toBe('single');
70
+ expect(list().getAttribute('span-count')).toBe('1');
71
+ expect(list().getAttribute('scroll-orientation')).toBe('vertical');
72
+ expect(list().classList).toContain('w-full');
73
+ expect(list().classList).toContain('bg-card');
74
+ });
75
+
76
+ it('gives the list an explicit height, which it needs to scroll', async () => {
77
+ const { host, list } = await renderList();
78
+ expect(list().getAttribute('style') ?? '').not.toContain('height');
79
+
80
+ host.height.set('400px');
81
+ await waitForUpdate();
82
+ expect(list().getAttribute('style')).toContain('height: 400px');
23
83
  });
24
84
 
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');
85
+ it('sets the gaps as list styles, which Lynx reads, not attributes', async () => {
86
+ // jsdom drops style properties it doesn't know, so the gaps never show in
87
+ // the style attribute: watch what reaches Lynx's inline style API instead.
88
+ const addInlineStyle = vi.spyOn(lynxMainThread(), '__AddInlineStyle');
89
+ const { host, list } = await renderList();
90
+ host.mainAxisGap.set('8px');
91
+ host.crossAxisGap.set('12px');
30
92
  await waitForUpdate();
31
93
 
32
- const list = container.querySelector('list');
33
- expect(list?.getAttribute('style')).toContain('height: 400px');
94
+ expect(addInlineStyle).toHaveBeenCalledWith(
95
+ list(),
96
+ 'list-main-axis-gap',
97
+ '8px',
98
+ );
99
+ expect(addInlineStyle).toHaveBeenCalledWith(
100
+ list(),
101
+ 'list-cross-axis-gap',
102
+ '12px',
103
+ );
104
+ expect(list().getAttribute('list-main-axis-gap')).toBeNull();
105
+ addInlineStyle.mockRestore();
106
+ });
107
+
108
+ it('hands Lynx real booleans, so the defaults that are on can be turned off', async () => {
109
+ // A false used to remove the attribute, which leaves Lynx's default: on.
110
+ const attributes = watchAttributes();
111
+ const { host } = await renderList();
112
+ expect(attributes.last('bounces')).toBe(true);
113
+ expect(attributes.last('scroll-bar-enable')).toBe(true);
114
+
115
+ host.bounces.set(false);
116
+ host.scrollBar.set(false);
117
+ host.nestedScroll.set(true);
118
+ host.sticky.set(true);
119
+ await waitForUpdate();
120
+ expect(attributes.last('bounces')).toBe(false);
121
+ expect(attributes.last('scroll-bar-enable')).toBe(false);
122
+ expect(attributes.last('enable-nested-scroll')).toBe(true);
123
+ expect(attributes.last('sticky')).toBe(true);
124
+ attributes.restore();
125
+ });
126
+
127
+ it('emits when scrolled to the start or the end', async () => {
128
+ const { host, list } = await renderList();
129
+ fireEvent.scrolltolower(list());
130
+ fireEvent.scrolltoupper(list());
131
+ expect(host.reached).toEqual(['end', 'start']);
34
132
  });
35
133
  });
@@ -11,25 +11,24 @@ import { cn } from '../../utils/cn';
11
11
 
12
12
  @Component({
13
13
  selector: 'ui-list',
14
- standalone: true,
15
14
  imports: [LYNX_ELEMENTS],
16
15
  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.
16
+ // The booleans are property bindings to <list>'s inputs, which hand Lynx a
17
+ // real `false`. They used to be `[attr.x]="x() || undefined"`, which removed
18
+ // the attribute when false, and a missing attribute means Lynx's default: on,
19
+ // for `bounces` and `scroll-bar-enable`. So neither could be turned off.
20
+ // (`[attr.x]` would send the string "false" instead.)
20
21
  template: `
21
22
  <list
22
23
  [attr.list-type]="listType()"
23
24
  [attr.span-count]="spanCount()"
24
25
  [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"
26
+ [bounces]="bounces()"
27
+ [scroll-bar-enable]="scrollBar()"
28
+ [enable-nested-scroll]="nestedScroll()"
29
+ [sticky]="sticky()"
29
30
  [attr.sticky-offset]="stickyOffset()"
30
31
  [attr.preload-buffer-count]="preloadBufferCount()"
31
- [attr.list-main-axis-gap]="mainAxisGap()"
32
- [attr.list-cross-axis-gap]="crossAxisGap()"
33
32
  [attr.lower-threshold-item-count]="lowerThresholdItemCount()"
34
33
  [attr.upper-threshold-item-count]="upperThresholdItemCount()"
35
34
  [class]="listClass()"
@@ -63,9 +62,16 @@ export class UiList {
63
62
 
64
63
  protected readonly listClass = computed(() => cn('w-full', this.userClass()));
65
64
 
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
- });
65
+ // Explicit height is required for list to scroll in Lynx. The gaps go here
66
+ // too: they're CSS properties that Lynx's list reads from its style, and as
67
+ // attributes they did nothing.
68
+ protected readonly listStyle = computed(() =>
69
+ [
70
+ this.height() && `height: ${this.height()};`,
71
+ this.mainAxisGap() && `list-main-axis-gap: ${this.mainAxisGap()};`,
72
+ this.crossAxisGap() && `list-cross-axis-gap: ${this.crossAxisGap()};`,
73
+ ]
74
+ .filter(Boolean)
75
+ .join(' '),
76
+ );
71
77
  }
@@ -0,0 +1,25 @@
1
+ export {
2
+ UI_MENU,
3
+ UiMenu,
4
+ UiMenuItem,
5
+ UiMenuSection,
6
+ UiMenuSeparator,
7
+ UiMenuTrigger,
8
+ type UiMenuHost,
9
+ } from './menu';
10
+ export {
11
+ UiMenuPopup,
12
+ measureMenuElement,
13
+ type MenuElement,
14
+ type MenuKind,
15
+ type MenuPhase,
16
+ } from './menu-popup';
17
+ export {
18
+ menuStartFrame,
19
+ placeMenu,
20
+ type MenuPlacement,
21
+ type MenuPlacementMode,
22
+ type MenuPlacementOptions,
23
+ type MenuRect,
24
+ type MenuSize,
25
+ } from './menu-placement';