panelui-native 0.71.0 → 0.73.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 (146) hide show
  1. package/README.md +2 -1
  2. package/lib/module/components/accordion/index.js +10 -4
  3. package/lib/module/components/accordion/index.js.map +1 -1
  4. package/lib/module/components/area-chart/index.js +1 -0
  5. package/lib/module/components/area-chart/index.js.map +1 -1
  6. package/lib/module/components/breadcrumb/index.js +2 -1
  7. package/lib/module/components/breadcrumb/index.js.map +1 -1
  8. package/lib/module/components/button/index.js +7 -5
  9. package/lib/module/components/button/index.js.map +1 -1
  10. package/lib/module/components/carousel/index.js +1 -1
  11. package/lib/module/components/carousel/index.js.map +1 -1
  12. package/lib/module/components/chip/index.js +2 -2
  13. package/lib/module/components/chip/index.js.map +1 -1
  14. package/lib/module/components/code-block/index.js.map +1 -1
  15. package/lib/module/components/collapsible/index.js +5 -1
  16. package/lib/module/components/collapsible/index.js.map +1 -1
  17. package/lib/module/components/color-picker/index.js.map +1 -1
  18. package/lib/module/components/fab/index.js +7 -6
  19. package/lib/module/components/fab/index.js.map +1 -1
  20. package/lib/module/components/flow/index.js +1 -1
  21. package/lib/module/components/flow/index.js.map +1 -1
  22. package/lib/module/components/frame/index.js +2 -2
  23. package/lib/module/components/frame/index.js.map +1 -1
  24. package/lib/module/components/grid-item/index.js +2 -2
  25. package/lib/module/components/grid-item/index.js.map +1 -1
  26. package/lib/module/components/heatmap-chart/index.js +1 -0
  27. package/lib/module/components/heatmap-chart/index.js.map +1 -1
  28. package/lib/module/components/hex-chart/index.js +1 -0
  29. package/lib/module/components/hex-chart/index.js.map +1 -1
  30. package/lib/module/components/input-group/index.js +7 -2
  31. package/lib/module/components/input-group/index.js.map +1 -1
  32. package/lib/module/components/item/index.js +2 -2
  33. package/lib/module/components/item/index.js.map +1 -1
  34. package/lib/module/components/line-chart/index.js +1 -0
  35. package/lib/module/components/line-chart/index.js.map +1 -1
  36. package/lib/module/components/marker/index.js +2 -2
  37. package/lib/module/components/marker/index.js.map +1 -1
  38. package/lib/module/components/marquee/index.js +9 -12
  39. package/lib/module/components/marquee/index.js.map +1 -1
  40. package/lib/module/components/marquee/marquee-math.js +39 -0
  41. package/lib/module/components/marquee/marquee-math.js.map +1 -0
  42. package/lib/module/components/menu/index.js +11 -3
  43. package/lib/module/components/menu/index.js.map +1 -1
  44. package/lib/module/components/pagination/index.js +27 -11
  45. package/lib/module/components/pagination/index.js.map +1 -1
  46. package/lib/module/components/panelside/index.js +7 -2
  47. package/lib/module/components/panelside/index.js.map +1 -1
  48. package/lib/module/components/planner/index.js +26 -5
  49. package/lib/module/components/planner/index.js.map +1 -1
  50. package/lib/module/components/plot/index.js +105 -42
  51. package/lib/module/components/plot/index.js.map +1 -1
  52. package/lib/module/components/post/index.js +4 -4
  53. package/lib/module/components/post/index.js.map +1 -1
  54. package/lib/module/components/search-bar/index.js +331 -0
  55. package/lib/module/components/search-bar/index.js.map +1 -0
  56. package/lib/module/components/table/index.js +3 -4
  57. package/lib/module/components/table/index.js.map +1 -1
  58. package/lib/module/components/timeline/index.js +288 -39
  59. package/lib/module/components/timeline/index.js.map +1 -1
  60. package/lib/module/components/toggle-button/index.js +1 -1
  61. package/lib/module/components/toggle-button/index.js.map +1 -1
  62. package/lib/module/components/tree/index.js +13 -8
  63. package/lib/module/components/tree/index.js.map +1 -1
  64. package/lib/module/hooks/use-keyboard-avoidance.js.map +1 -1
  65. package/lib/module/index.js +1 -0
  66. package/lib/module/index.js.map +1 -1
  67. package/lib/module/utils/chart.js +50 -7
  68. package/lib/module/utils/chart.js.map +1 -1
  69. package/lib/typescript/src/components/accordion/index.d.ts +2 -2
  70. package/lib/typescript/src/components/accordion/index.d.ts.map +1 -1
  71. package/lib/typescript/src/components/area-chart/index.d.ts.map +1 -1
  72. package/lib/typescript/src/components/breadcrumb/index.d.ts.map +1 -1
  73. package/lib/typescript/src/components/button/index.d.ts.map +1 -1
  74. package/lib/typescript/src/components/code-block/index.d.ts +3 -3
  75. package/lib/typescript/src/components/code-block/index.d.ts.map +1 -1
  76. package/lib/typescript/src/components/collapsible/index.d.ts +2 -2
  77. package/lib/typescript/src/components/collapsible/index.d.ts.map +1 -1
  78. package/lib/typescript/src/components/color-picker/index.d.ts.map +1 -1
  79. package/lib/typescript/src/components/fab/index.d.ts +4 -2
  80. package/lib/typescript/src/components/fab/index.d.ts.map +1 -1
  81. package/lib/typescript/src/components/heatmap-chart/index.d.ts.map +1 -1
  82. package/lib/typescript/src/components/hex-chart/index.d.ts.map +1 -1
  83. package/lib/typescript/src/components/input-group/index.d.ts.map +1 -1
  84. package/lib/typescript/src/components/line-chart/index.d.ts.map +1 -1
  85. package/lib/typescript/src/components/marquee/index.d.ts +1 -1
  86. package/lib/typescript/src/components/marquee/index.d.ts.map +1 -1
  87. package/lib/typescript/src/components/marquee/marquee-math.d.ts +21 -0
  88. package/lib/typescript/src/components/marquee/marquee-math.d.ts.map +1 -0
  89. package/lib/typescript/src/components/menu/index.d.ts +1 -1
  90. package/lib/typescript/src/components/menu/index.d.ts.map +1 -1
  91. package/lib/typescript/src/components/pagination/index.d.ts.map +1 -1
  92. package/lib/typescript/src/components/panelside/index.d.ts +1 -1
  93. package/lib/typescript/src/components/panelside/index.d.ts.map +1 -1
  94. package/lib/typescript/src/components/plan/index.d.ts +2 -2
  95. package/lib/typescript/src/components/planner/index.d.ts.map +1 -1
  96. package/lib/typescript/src/components/plot/index.d.ts +59 -5
  97. package/lib/typescript/src/components/plot/index.d.ts.map +1 -1
  98. package/lib/typescript/src/components/search-bar/index.d.ts +216 -0
  99. package/lib/typescript/src/components/search-bar/index.d.ts.map +1 -0
  100. package/lib/typescript/src/components/timeline/index.d.ts +35 -2
  101. package/lib/typescript/src/components/timeline/index.d.ts.map +1 -1
  102. package/lib/typescript/src/components/tree/index.d.ts +3 -4
  103. package/lib/typescript/src/components/tree/index.d.ts.map +1 -1
  104. package/lib/typescript/src/hooks/use-keyboard-avoidance.d.ts +2 -2
  105. package/lib/typescript/src/hooks/use-keyboard-avoidance.d.ts.map +1 -1
  106. package/lib/typescript/src/index.d.ts +2 -1
  107. package/lib/typescript/src/index.d.ts.map +1 -1
  108. package/lib/typescript/src/utils/chart.d.ts +28 -3
  109. package/lib/typescript/src/utils/chart.d.ts.map +1 -1
  110. package/package.json +1 -1
  111. package/src/components/accordion/index.tsx +17 -7
  112. package/src/components/area-chart/index.tsx +8 -0
  113. package/src/components/breadcrumb/index.tsx +2 -1
  114. package/src/components/button/index.tsx +3 -2
  115. package/src/components/carousel/index.tsx +1 -1
  116. package/src/components/chip/index.tsx +2 -2
  117. package/src/components/code-block/index.tsx +3 -3
  118. package/src/components/collapsible/index.tsx +13 -4
  119. package/src/components/color-picker/index.tsx +3 -2
  120. package/src/components/fab/index.tsx +21 -11
  121. package/src/components/flow/index.tsx +1 -1
  122. package/src/components/frame/index.tsx +2 -2
  123. package/src/components/grid-item/index.tsx +2 -2
  124. package/src/components/heatmap-chart/index.tsx +7 -1
  125. package/src/components/hex-chart/index.tsx +7 -1
  126. package/src/components/input-group/index.tsx +7 -2
  127. package/src/components/item/index.tsx +2 -2
  128. package/src/components/line-chart/index.tsx +9 -0
  129. package/src/components/marker/index.tsx +2 -2
  130. package/src/components/marquee/index.tsx +14 -12
  131. package/src/components/marquee/marquee-math.ts +37 -0
  132. package/src/components/menu/index.tsx +11 -3
  133. package/src/components/pagination/index.tsx +26 -13
  134. package/src/components/panelside/index.tsx +7 -2
  135. package/src/components/plan/index.tsx +2 -2
  136. package/src/components/planner/index.tsx +20 -5
  137. package/src/components/plot/index.tsx +183 -39
  138. package/src/components/post/index.tsx +4 -4
  139. package/src/components/search-bar/index.tsx +436 -0
  140. package/src/components/table/index.tsx +2 -2
  141. package/src/components/timeline/index.tsx +335 -39
  142. package/src/components/toggle-button/index.tsx +1 -1
  143. package/src/components/tree/index.tsx +17 -12
  144. package/src/hooks/use-keyboard-avoidance.ts +7 -2
  145. package/src/index.ts +2 -0
  146. package/src/utils/chart.ts +66 -7
@@ -1412,6 +1412,7 @@ function PanelsideTrigger({
1412
1412
  className,
1413
1413
  label = 'Open navigation panel',
1414
1414
  children,
1415
+ onPress,
1415
1416
  ...props
1416
1417
  }: PanelsideTriggerProps) {
1417
1418
  const { toggle, docked } = usePanelsideContext('Panelside.Trigger');
@@ -1426,6 +1427,7 @@ function PanelsideTrigger({
1426
1427
  return cloneElement(children, {
1427
1428
  onPress: (...args: unknown[]) => {
1428
1429
  children.props.onPress?.(...args);
1430
+ onPress?.(...(args as Parameters<NonNullable<PressableProps['onPress']>>));
1429
1431
  toggle();
1430
1432
  },
1431
1433
  });
@@ -1433,11 +1435,14 @@ function PanelsideTrigger({
1433
1435
 
1434
1436
  return (
1435
1437
  <AnimatedPressable
1436
- onPress={toggle}
1438
+ {...props}
1439
+ onPress={(event) => {
1440
+ onPress?.(event);
1441
+ toggle();
1442
+ }}
1437
1443
  className={cn('h-10 w-10 items-center justify-center rounded-full', className)}
1438
1444
  accessibilityRole="button"
1439
1445
  accessibilityLabel={label}
1440
- {...props}
1441
1446
  >
1442
1447
  <MenuIcon size={20} color={color} />
1443
1448
  </AnimatedPressable>
@@ -285,7 +285,7 @@ function PlanIcon({ className, children, ...props }: PlanIconProps) {
285
285
  );
286
286
  }
287
287
 
288
- export interface PlanTitleProps extends ViewProps {
288
+ export interface PlanTitleProps extends TextProps {
289
289
  className?: string;
290
290
  children?: ReactNode;
291
291
  }
@@ -306,7 +306,7 @@ function PlanTitle({ className, children, ...props }: PlanTitleProps) {
306
306
  );
307
307
  }
308
308
 
309
- export interface PlanDescriptionProps extends ViewProps {
309
+ export interface PlanDescriptionProps extends TextProps {
310
310
  className?: string;
311
311
  children?: ReactNode;
312
312
  }
@@ -149,7 +149,13 @@ const dayVariants = tv({
149
149
  * Square-ish because a day here holds a number, a marker and sometimes an
150
150
  * icon, and a circle wastes the corners it needs for them.
151
151
  */
152
- cell: 'mx-0.5 h-14 flex-1 rounded-xl px-1.5 pt-1.5',
152
+ /*
153
+ * The transparent border is load-bearing: today and the open day both draw
154
+ * one, and a cell that only grows a border when it is picked shifts its
155
+ * contents by a point at the moment you look at it. Every cell reserves the
156
+ * point; the variants below only colour it.
157
+ */
158
+ cell: 'mx-0.5 h-14 flex-1 rounded-xl border border-transparent px-1.5 pt-1.5',
153
159
  number: 'text-xs leading-none',
154
160
  marker: 'absolute right-1.5 top-1.5 h-1.5 w-1.5 rounded-full',
155
161
  body: 'flex-1 flex-row items-center justify-center gap-0.5 pb-1',
@@ -161,13 +167,22 @@ const dayVariants = tv({
161
167
  false: { cell: 'bg-muted/15', number: 'text-muted-foreground/40' },
162
168
  },
163
169
  /*
164
- * Today is the number, not the tile. A ring on today and a ring on the
165
- * open day are two rings that mean different things and look the same.
170
+ * Today rings the tile; the open day fills it. Two rings would be the
171
+ * problem — two channels are not, and the ring is the one that has to
172
+ * survive being read at a glance across forty-two tiles.
166
173
  */
167
- today: { true: { number: 'text-primary font-semibold' } },
168
- selected: { true: { cell: 'border border-primary/60 bg-primary/10' } },
174
+ today: { true: { cell: 'border-foreground', number: 'font-semibold' } },
175
+ selected: { true: { cell: 'border-primary/60 bg-primary/10' } },
169
176
  disabled: { true: { cell: 'opacity-40' } },
170
177
  },
178
+ compoundVariants: [
179
+ /*
180
+ * Today, and open. Declaration order would otherwise hand the border to
181
+ * `selected` and lose the one mark that says which day it actually is —
182
+ * so the ring is restated over the fill.
183
+ */
184
+ { today: true, selected: true, class: { cell: 'border-foreground bg-primary/10' } },
185
+ ],
171
186
  defaultVariants: { inMonth: true },
172
187
  });
173
188
 
@@ -105,6 +105,7 @@ import {
105
105
  columnValues,
106
106
  compactNumber,
107
107
  linePath,
108
+ niceDomain,
108
109
  useSeriesColor,
109
110
  xOf,
110
111
  yOf,
@@ -281,6 +282,17 @@ export interface PlotProps extends ViewProps {
281
282
  * arrives.
282
283
  */
283
284
  yDomain?: [PlotBound, PlotBound];
285
+ /**
286
+ * Round the derived ends of the y-domain out to whole numbers.
287
+ *
288
+ * Left off, an axis ends a tenth of the span past the largest value, so it
289
+ * gets labelled 34,650 — true, and not a number anybody was looking for. On,
290
+ * the ends move out to a step of 1, 2 or 5 times a power of ten, and the
291
+ * labels become values a reader can measure against.
292
+ *
293
+ * It only ever widens the axis, and a pinned end is left alone.
294
+ */
295
+ nice?: boolean;
284
296
  /**
285
297
  * How an index becomes an x. Derived from the marks when left out: a plot
286
298
  * with bars in it is banded, and anything else is on points.
@@ -317,6 +329,7 @@ const PlotRoot = forwardRef<PlotHandle, PlotProps>(function PlotRoot(
317
329
  animationDuration = 700,
318
330
  domainDuration = 500,
319
331
  yDomain,
332
+ nice = false,
320
333
  xScale: xScaleProp,
321
334
  curve = 'monotone',
322
335
  onActiveIndexChange,
@@ -450,11 +463,20 @@ const PlotRoot = forwardRef<PlotHandle, PlotProps>(function PlotRoot(
450
463
  * drifting off the axis is exactly what pinning it was for.
451
464
  */
452
465
  const headroom = (max - min) * 0.1;
466
+ /*
467
+ * `nice` replaces the headroom rather than adding to it: rounding out to a
468
+ * whole step is already room, and doing both would leave a tenth of the
469
+ * span of empty axis above a top the reader was told is round.
470
+ */
471
+ const [niceLow, niceHigh] = nice ? niceDomain(min, max) : [min, max];
472
+ const derivedLow = nice ? niceLow : min - headroom * lowRoom;
473
+ const derivedHigh = nice ? niceHigh : max + headroom * highRoom;
474
+
453
475
  return [
454
- typeof lowPin === 'number' ? lowPin : min - headroom * lowRoom,
455
- typeof highPin === 'number' ? highPin : max + headroom * highRoom,
476
+ typeof lowPin === 'number' ? lowPin : derivedLow,
477
+ typeof highPin === 'number' ? highPin : derivedHigh,
456
478
  ];
457
- }, [data, yDomain, seriesKeys, hasBars]);
479
+ }, [data, yDomain, seriesKeys, hasBars, nice]);
458
480
 
459
481
  const loading = status === 'loading';
460
482
  // Nothing has said what the axis is yet — no mark has registered and no
@@ -603,11 +625,19 @@ const PlotRoot = forwardRef<PlotHandle, PlotProps>(function PlotRoot(
603
625
  * read as the chart arriving, rather than as four separate
604
626
  * things animating in at once — which is the thing a composed
605
627
  * chart is most at risk of looking like.
628
+ *
629
+ * `width` is set statically as well as animated, and it has
630
+ * to be. Animated props on an element inside `Defs` do not
631
+ * reach the native clip on every platform, and with the width
632
+ * coming only from the animation there is no width at all
633
+ * when they do not — an empty clip, and marks that never
634
+ * appear while the axes draw normally.
606
635
  */}
607
636
  <ClipPath id={clipId}>
608
637
  <AnimatedRect
609
638
  x={plot.left}
610
639
  y={0}
640
+ width={plot.width}
611
641
  height={size.height}
612
642
  animatedProps={clipProps}
613
643
  />
@@ -759,28 +789,12 @@ function PlotLine({
759
789
  const max = domainMax.value;
760
790
  if (max === min || plot.width <= 0) return { d: '' };
761
791
 
762
- if (!banded) return { d: linePath(values, plot, min, max, curve, false) };
763
-
764
792
  /*
765
- * A banded line is built here rather than through `linePath`, which spreads
766
- * its points across the full width. On a band scale the points belong in
767
- * the middle of their own slice — a line that ignored that would start half
768
- * a slice left of the bar it is describing.
793
+ * `banded` moves the points to the middle of their own slice, which is
794
+ * where a line drawn over columns has to sit — spread edge to edge it
795
+ * starts half a slice left of the bar it is describing.
769
796
  */
770
- let d = '';
771
- let drawn = 0;
772
- for (let index = 0; index < values.length; index += 1) {
773
- const value = values[index];
774
- if (value === null || value === undefined) {
775
- drawn = 0;
776
- continue;
777
- }
778
- const x = bandOf(index, values.length, plot);
779
- const y = yOf(value, plot, min, max);
780
- d += `${drawn === 0 ? 'M' : 'L'}${x},${y}`;
781
- drawn += 1;
782
- }
783
- return { d };
797
+ return { d: linePath(values, plot, min, max, curve, false, undefined, banded) };
784
798
  });
785
799
 
786
800
  if (status === 'loading') return null;
@@ -820,15 +834,19 @@ function PlotArea({
820
834
  opacity = 0.18,
821
835
  curve = 'monotone',
822
836
  }: PlotAreaProps) {
823
- const { data, plot, domainMin, domainMax, status, clipId } = useChart('Plot.Area');
837
+ const { data, plot, xScale, domainMin, domainMax, status, clipId } =
838
+ useChart('Plot.Area');
824
839
  const fill = useMark(dataKey, color, colorIndex);
825
840
  const values = useMemo(() => columnValues(data, dataKey), [data, dataKey]);
841
+ const banded = xScale === 'band';
826
842
 
827
843
  const animatedProps = useAnimatedProps(() => {
828
844
  const min = domainMin.value;
829
845
  const max = domainMax.value;
830
846
  if (max === min || plot.width <= 0) return { d: '' };
831
- return { d: areaPath(values, plot, min, max, curve, false) };
847
+ // Band-aware for the same reason the line is: an area on a plot that also
848
+ // carries columns has to close over the same slices they occupy.
849
+ return { d: areaPath(values, plot, min, max, curve, false, undefined, banded) };
832
850
  });
833
851
 
834
852
  if (status === 'loading') return null;
@@ -848,6 +866,18 @@ export interface PlotBarsProps extends PlotSeriesProps {
848
866
  /** Rounds the end the bar grows towards, in points. */
849
867
  radius?: number;
850
868
  opacity?: number;
869
+ /**
870
+ * The value the columns grow from. Zero by default, and zero is nearly always
871
+ * right — a bar is a length, and a length has to start where the quantity
872
+ * does.
873
+ *
874
+ * Set it for the case where the reader is being shown movement rather than
875
+ * size: temperatures against a seasonal average, a score against a pass mark.
876
+ * Columns then run up and down from that line instead of all standing on the
877
+ * floor. It is clamped into the axis, so a baseline the domain does not cover
878
+ * falls back to the nearer edge.
879
+ */
880
+ baseline?: number;
851
881
  }
852
882
 
853
883
  /**
@@ -866,6 +896,7 @@ function PlotBars({
866
896
  gap = 0.35,
867
897
  radius = 4,
868
898
  opacity = 1,
899
+ baseline = 0,
869
900
  }: PlotBarsProps) {
870
901
  const { data, plot, domainMin, domainMax, status, clipId } = useChart('Plot.Bars');
871
902
  const fill = useMark(dataKey, color, colorIndex);
@@ -881,12 +912,12 @@ function PlotBars({
881
912
  if (max === min || plot.width <= 0 || total === 0) return { d: '' };
882
913
 
883
914
  /*
884
- * Bars grow from the baseline, which is the domain's zero where the domain
885
- * covers it and the nearer edge where it does not. A bar drawn from the
915
+ * Bars grow from the baseline — zero unless the caller moved it — clamped
916
+ * into the domain where the axis does not reach it. A bar drawn from the
886
917
  * bottom of a plot whose axis starts at 40 is a length that is not in the
887
918
  * data.
888
919
  */
889
- const base = yOf(Math.min(Math.max(0, min), max), plot, min, max);
920
+ const base = yOf(Math.min(Math.max(baseline, min), max), plot, min, max);
890
921
  let path = '';
891
922
 
892
923
  for (let index = 0; index < total; index += 1) {
@@ -1059,11 +1090,36 @@ PlotOverlay.displayName = 'Plot.Overlay';
1059
1090
  PlotOverlay.layer = 'overlay' as Layer;
1060
1091
 
1061
1092
  export interface PlotRuleProps {
1062
- /** Where to draw it, in the data's own units. */
1063
- y: number;
1093
+ /**
1094
+ * Where to draw it, in the data's own units. Omit it and pass `x` instead for
1095
+ * a rule down the plot rather than across it.
1096
+ */
1097
+ y?: number;
1098
+ /**
1099
+ * A row to draw a vertical rule at, by index — the release the numbers are
1100
+ * read against, the day a change landed.
1101
+ *
1102
+ * The x axis here carries positions rather than quantities, so this is which
1103
+ * row rather than what value. Exactly one of `y` and `x` is drawn; `y` wins
1104
+ * if both are given.
1105
+ */
1106
+ x?: number;
1064
1107
  /** A name for what the line means. Nothing is drawn without one. */
1065
1108
  label?: string;
1109
+ /** Overrides the line *and* its caption, so the two cannot drift apart. */
1066
1110
  color?: string;
1111
+ /** Thickness in points. */
1112
+ strokeWidth?: number;
1113
+ /**
1114
+ * Break the line into dashes, for a rule that should read as an annotation
1115
+ * rather than as a series the chart is drawing.
1116
+ */
1117
+ dashed?: boolean;
1118
+ /** Fades the line and its caption together. */
1119
+ opacity?: number;
1120
+ /** Which end of the rule the caption sits at. */
1121
+ labelPlacement?: 'start' | 'end';
1122
+ labelClassName?: string;
1067
1123
  className?: string;
1068
1124
  }
1069
1125
 
@@ -1071,15 +1127,34 @@ export interface PlotRuleProps {
1071
1127
  * A reference line across the plot — a target, a limit, an average.
1072
1128
  *
1073
1129
  * A view rather than an SVG line, so its label is real text and follows the
1074
- * theme. Its height is one point and its position is a transform, so it costs
1130
+ * theme. Its thickness is a border and its position is a transform, so it costs
1075
1131
  * no more than the SVG line would while gaining a legible caption.
1132
+ *
1133
+ * It is drawn at full strength in the foreground colour, and it is meant to be.
1134
+ * A reference line is the number the series is being judged against — a target
1135
+ * nobody can read is a target the chart is not actually stating — so what keeps
1136
+ * it from being mistaken for a series is that it is neutral and optionally
1137
+ * dashed, not that it is faint.
1076
1138
  */
1077
- function PlotRule({ y, label, color, className }: PlotRuleProps) {
1078
- const { plot, domainMin, domainMax, status } = useChart('Plot.Rule');
1079
- const token = useCSSVariable('--color-muted-foreground');
1139
+ function PlotRule({
1140
+ y,
1141
+ x,
1142
+ label,
1143
+ color,
1144
+ strokeWidth = 1,
1145
+ dashed = false,
1146
+ opacity = 1,
1147
+ labelPlacement = 'end',
1148
+ labelClassName,
1149
+ className,
1150
+ }: PlotRuleProps) {
1151
+ const { plot, domainMin, domainMax, status, data } = useChart('Plot.Rule');
1152
+ const token = useCSSVariable('--color-foreground');
1080
1153
  const stroke = color ?? (typeof token === 'string' ? token : '#888888');
1154
+ const vertical = y === undefined && x !== undefined;
1081
1155
 
1082
- const style = useAnimatedStyle(() => {
1156
+ const horizontalStyle = useAnimatedStyle(() => {
1157
+ if (vertical || y === undefined) return { opacity: 0 };
1083
1158
  const min = domainMin.value;
1084
1159
  const max = domainMax.value;
1085
1160
  if (max === min) return { opacity: 0 };
@@ -1087,23 +1162,92 @@ function PlotRule({ y, label, color, className }: PlotRuleProps) {
1087
1162
  // Off the top or bottom of the plot is out of the chart's range, and a rule
1088
1163
  // pinned to the edge would claim a value the axis does not cover.
1089
1164
  const inside = at >= plot.top - 0.5 && at <= plot.top + plot.height + 0.5;
1090
- return { opacity: inside ? 1 : 0, transform: [{ translateY: at }] };
1165
+ return { opacity: inside ? opacity : 0, transform: [{ translateY: at }] };
1091
1166
  });
1092
1167
 
1093
1168
  if (status === 'loading') return null;
1094
1169
 
1170
+ const line = dashed
1171
+ ? {
1172
+ borderColor: stroke,
1173
+ borderStyle: 'dashed' as const,
1174
+ borderTopWidth: strokeWidth,
1175
+ }
1176
+ : { backgroundColor: stroke, height: strokeWidth };
1177
+
1178
+ if (vertical) {
1179
+ /*
1180
+ * The x axis carries positions, not quantities, so a vertical rule is
1181
+ * placed from the row's own geometry rather than from the tweening domain —
1182
+ * there is nothing on this axis for it to tween against.
1183
+ */
1184
+ const total = data.length;
1185
+ const at =
1186
+ total > 0
1187
+ ? xOf(Math.max(0, Math.min(total - 1, x!)), total, plot)
1188
+ : plot.left + plot.width / 2;
1189
+
1190
+ return (
1191
+ <View
1192
+ pointerEvents="none"
1193
+ style={{
1194
+ position: 'absolute',
1195
+ left: at,
1196
+ top: plot.top,
1197
+ height: plot.height,
1198
+ opacity,
1199
+ }}
1200
+ className={className}
1201
+ >
1202
+ <View
1203
+ style={
1204
+ dashed
1205
+ ? {
1206
+ borderColor: stroke,
1207
+ borderStyle: 'dashed',
1208
+ borderLeftWidth: strokeWidth,
1209
+ flex: 1,
1210
+ }
1211
+ : { backgroundColor: stroke, width: strokeWidth, flex: 1 }
1212
+ }
1213
+ />
1214
+ {label ? (
1215
+ <Text
1216
+ size="xs"
1217
+ weight="medium"
1218
+ numberOfLines={1}
1219
+ style={color ? { color } : undefined}
1220
+ className={cn('absolute left-1 top-0', labelClassName)}
1221
+ >
1222
+ {label}
1223
+ </Text>
1224
+ ) : null}
1225
+ </View>
1226
+ );
1227
+ }
1228
+
1095
1229
  return (
1096
1230
  <Animated.View
1097
1231
  pointerEvents="none"
1098
1232
  style={[
1099
1233
  { position: 'absolute', left: plot.left, width: plot.width, top: 0 },
1100
- style,
1234
+ horizontalStyle,
1101
1235
  ]}
1102
1236
  className={className}
1103
1237
  >
1104
- <View style={{ height: 1, backgroundColor: stroke, opacity: 0.5 }} />
1238
+ <View style={line} />
1105
1239
  {label ? (
1106
- <Text size="xs" muted numberOfLines={1} className="self-end pt-0.5">
1240
+ <Text
1241
+ size="xs"
1242
+ weight="medium"
1243
+ numberOfLines={1}
1244
+ style={color ? { color } : undefined}
1245
+ className={cn(
1246
+ 'pt-0.5',
1247
+ labelPlacement === 'start' ? 'self-start' : 'self-end',
1248
+ labelClassName
1249
+ )}
1250
+ >
1107
1251
  {label}
1108
1252
  </Text>
1109
1253
  ) : null}
@@ -198,7 +198,7 @@ const PostRoot = forwardRef<View, PostProps>(
198
198
 
199
199
  if (!onPress) {
200
200
  return (
201
- <View ref={ref} className={root({ className })} {...props}>
201
+ <View ref={ref} {...props} className={root({ className })}>
202
202
  {body}
203
203
  </View>
204
204
  );
@@ -210,10 +210,10 @@ const PostRoot = forwardRef<View, PostProps>(
210
210
  // as one button buries them.
211
211
  <AnimatedPressable
212
212
  ref={ref}
213
+ {...props}
213
214
  accessibilityRole="link"
214
215
  onPress={onPress}
215
216
  className={root({ className })}
216
- {...props}
217
217
  >
218
218
  {body}
219
219
  </AnimatedPressable>
@@ -681,7 +681,7 @@ const PostMedia = forwardRef<View, PostMediaProps>(
681
681
 
682
682
  if (!onPress) {
683
683
  return (
684
- <View ref={ref} className={media({ className })} style={{ aspectRatio }} {...props}>
684
+ <View ref={ref} {...props} className={media({ className })} style={{ aspectRatio }}>
685
685
  {picture}
686
686
  </View>
687
687
  );
@@ -690,12 +690,12 @@ const PostMedia = forwardRef<View, PostMediaProps>(
690
690
  return (
691
691
  <Pressable
692
692
  ref={ref}
693
+ {...props}
693
694
  accessibilityRole="imagebutton"
694
695
  accessibilityLabel={alt}
695
696
  onPress={onPress}
696
697
  className={media({ className })}
697
698
  style={{ aspectRatio }}
698
- {...props}
699
699
  >
700
700
  {picture}
701
701
  </Pressable>