@pallasoft/ps-mobile-components 1.10.1 → 1.11.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 (40) hide show
  1. package/dist/components/ps-cartesian-chart/ps-cartesian-chart-props.d.ts +1 -0
  2. package/dist/components/ps-cartesian-chart/ps-cartesian-chart.d.ts +1 -1
  3. package/dist/components/ps-cartesian-chart/ps-cartesian-chart.js +6 -5
  4. package/dist/components/ps-chart-frame/ps-chart-frame.js +5 -1
  5. package/dist/components/ps-donut-chart/ps-donut-chart-props.d.ts +1 -0
  6. package/dist/components/ps-donut-chart/ps-donut-chart.d.ts +1 -1
  7. package/dist/components/ps-donut-chart/ps-donut-chart.js +2 -2
  8. package/dist/components/ps-header-menu/ps-header-menu.js +2 -0
  9. package/dist/components/ps-menu-popup/ps-menu-popup.js +10 -8
  10. package/dist/components/ps-sheet-presenter/ps-sheet-presenter.ios.js +8 -1
  11. package/dist/components/ps-sparkline/ps-sparkline-props.d.ts +1 -0
  12. package/dist/components/ps-sparkline/ps-sparkline.d.ts +1 -1
  13. package/dist/components/ps-sparkline/ps-sparkline.js +2 -2
  14. package/dist/components/ps-waterfall-chart/ps-waterfall-chart-props.d.ts +1 -0
  15. package/dist/components/ps-waterfall-chart/ps-waterfall-chart.d.ts +1 -1
  16. package/dist/components/ps-waterfall-chart/ps-waterfall-chart.js +6 -5
  17. package/dist/constants/ps-chart-config.d.ts +3 -0
  18. package/dist/constants/ps-chart-config.js +3 -0
  19. package/dist/hooks/use-ps-chart-reveal.d.ts +1 -1
  20. package/dist/hooks/use-ps-chart-reveal.js +4 -2
  21. package/dist/hooks/use-ps-chart-scrub.js +17 -18
  22. package/dist/hooks/use-ps-infinite-data.d.ts +1 -1
  23. package/dist/hooks/use-ps-infinite-data.js +23 -7
  24. package/dist/hooks/use-ps-keyboard-height.d.ts +1 -0
  25. package/dist/hooks/use-ps-keyboard-height.js +14 -0
  26. package/dist/interfaces/ps-chart-scrub-result.d.ts +2 -2
  27. package/dist/interfaces/ps-infinite-data-options.d.ts +1 -0
  28. package/dist/interfaces/ps-infinite-data-result.d.ts +1 -0
  29. package/dist/interfaces/ps-menu-popup-insets.d.ts +4 -0
  30. package/dist/interfaces/ps-menu-popup-insets.js +1 -0
  31. package/dist/services/ps-chart-series-service.d.ts +0 -1
  32. package/dist/services/ps-chart-series-service.js +0 -3
  33. package/dist/services/ps-infinite-data-service.d.ts +1 -0
  34. package/dist/services/ps-infinite-data-service.js +3 -0
  35. package/dist/services/ps-menu-popup-service.d.ts +2 -1
  36. package/dist/services/ps-menu-popup-service.js +3 -3
  37. package/dist/services/ps-sheet-service.d.ts +1 -0
  38. package/dist/services/ps-sheet-service.js +3 -0
  39. package/jest-setup.js +4 -0
  40. package/package.json +1 -1
@@ -9,6 +9,7 @@ export interface PsCartesianChartProps {
9
9
  height?: number;
10
10
  isLoading?: boolean;
11
11
  kind: PsCartesianChartKind;
12
+ revealKey?: string;
12
13
  series: PsCartesianChartSeries[];
13
14
  showLegend?: boolean;
14
15
  }
@@ -1,2 +1,2 @@
1
1
  import type { PsCartesianChartProps } from './ps-cartesian-chart-props';
2
- export declare const PsCartesianChart: ({ data, emptyLabel, formatTick, formatValue, height: baseHeight, isLoading, kind, series, showLegend, }: PsCartesianChartProps) => import("react").JSX.Element;
2
+ export declare const PsCartesianChart: ({ data, emptyLabel, formatTick, formatValue, height: baseHeight, isLoading, kind, revealKey, series, showLegend, }: PsCartesianChartProps) => import("react").JSX.Element;
@@ -1,5 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { StyleSheet, View } from 'react-native';
3
+ import { GestureDetector } from 'react-native-gesture-handler';
3
4
  import Svg, { Circle, G, Line, Text as SvgText } from 'react-native-svg';
4
5
  import { PsChartConfig } from '../../constants/ps-chart-config';
5
6
  import { PsCartesianChartKind } from '../../constants/ps-chart-kind';
@@ -22,7 +23,7 @@ import { PsChartLegend } from '../ps-chart-legend/ps-chart-legend';
22
23
  import { PsChartLine } from '../ps-chart-line/ps-chart-line';
23
24
  import { PsChartReadout } from '../ps-chart-readout/ps-chart-readout';
24
25
  import { PsChartSkeleton } from '../ps-chart-skeleton/ps-chart-skeleton';
25
- export const PsCartesianChart = ({ data, emptyLabel, formatTick, formatValue, height: baseHeight = PsChartConfig.chartHeight, isLoading = false, kind, series, showLegend = true, }) => {
26
+ export const PsCartesianChart = ({ data, emptyLabel, formatTick, formatValue, height: baseHeight = PsChartConfig.chartHeight, isLoading = false, kind, revealKey, series, showLegend = true, }) => {
26
27
  const theme = usePsMobileTheme();
27
28
  const { onLayout, width } = usePsChartWidth();
28
29
  const height = baseHeight + usePsWidgetFill();
@@ -41,8 +42,8 @@ export const PsCartesianChart = ({ data, emptyLabel, formatTick, formatValue, he
41
42
  const tickLabels = PsChartScaleService.ticks(domain).map((tick) => formatTick(tick));
42
43
  const yAxisWidth = PsChartAxisService.yAxisWidth(tickLabels, width);
43
44
  const plot = PsChartScaleService.plot(width, height, yAxisWidth);
44
- const { index, panHandlers } = usePsChartScrub(data.length, isBars, plot);
45
- const progress = usePsChartReveal(PsChartSeriesService.signature(kind, data, series));
45
+ const { gesture, index } = usePsChartScrub(data.length, isBars, plot);
46
+ const progress = usePsChartReveal(!isLoading && width > 0, revealKey);
46
47
  if (isLoading)
47
48
  return (_jsx(PsChartSkeleton, { height: baseHeight, variant: PsChartKindService.skeletonVariant(kind) }));
48
49
  if (data.length === 0 || PsChartSeriesService.isEmpty(data, series))
@@ -65,7 +66,7 @@ export const PsCartesianChart = ({ data, emptyLabel, formatTick, formatValue, he
65
66
  : isStacked
66
67
  ? PsChartBarService.stacked(data, barSeries, barColors, domain, plot)
67
68
  : PsChartBarService.grouped(data, barSeries, barColors, domain, plot);
68
- return (_jsxs(View, { style: { gap: 8 }, children: [_jsx(PsChartReadout, { items: items, lines: readoutLines, title: datum.label }), _jsx(View, { accessible: true, accessibilityLabel: PsChartReadoutService.summary(datum.label, items), accessibilityRole: 'image', onLayout: onLayout, style: { height }, children: width === 0 ? null : (_jsxs(_Fragment, { children: [_jsxs(Svg, { height: height, pointerEvents: 'none', width: width, children: [PsChartScaleService.ticks(domain).map((tick, tickIndex) => {
69
+ return (_jsxs(View, { style: { gap: 8 }, children: [_jsx(PsChartReadout, { items: items, lines: readoutLines, title: datum.label }), _jsx(View, { accessible: true, accessibilityLabel: PsChartReadoutService.summary(datum.label, items), accessibilityRole: 'image', onLayout: onLayout, style: { height }, children: width === 0 ? (_jsx(PsChartSkeleton, { height: baseHeight, variant: PsChartKindService.skeletonVariant(kind) })) : (_jsxs(_Fragment, { children: [_jsxs(Svg, { height: height, pointerEvents: 'none', width: width, children: [PsChartScaleService.ticks(domain).map((tick, tickIndex) => {
69
70
  const y = PsChartScaleService.y(tick, domain, plot);
70
71
  return (_jsxs(G, { children: [_jsx(Line, { stroke: theme.line, strokeWidth: PsChartConfig.gridLineWidth, x1: plot.left, x2: plot.right, y1: y, y2: y }), _jsx(SvgText, { fill: theme.muted, fontFamily: theme.fonts.medium, fontSize: PsChartConfig.axisLabelSize, textAnchor: 'end', x: plot.left - PsChartConfig.axisTickGap, y: y + PsChartConfig.axisLabelSize / 3, children: PsChartTextService.fit(tickLabels[tickIndex], plot.left - PsChartConfig.axisTickGap, PsChartConfig.axisLabelSize) })] }, tick));
71
72
  }), xLabels.indexes.map((position) => (_jsx(SvgText, { fill: theme.muted, fontFamily: theme.fonts.medium, fontSize: PsChartConfig.axisLabelSize, textAnchor: PsChartScaleService.labelAnchor(position, data.length, isBars), x: xAt(position), y: height - 6, children: PsChartTextService.fit(data[position].label, xLabels.maxWidth, PsChartConfig.axisLabelSize) }, position))), rects.map((rect) => (_jsx(PsChartBar, { progress: progress, rect: rect }, rect.id))), lineSeries.map((entry, lineIndex) => {
@@ -77,7 +78,7 @@ export const PsCartesianChart = ({ data, emptyLabel, formatTick, formatValue, he
77
78
  return (_jsx(PsChartLine, { areaBaseline: kind === PsCartesianChartKind.Area
78
79
  ? PsChartScaleService.y(0, domain, plot)
79
80
  : undefined, color: colors[position], points: points, progress: progress }, entry.dataKey));
80
- }), index === null ? null : (_jsx(Line, { stroke: theme.muted, strokeWidth: PsChartConfig.scrubLineWidth, x1: xAt(selected), x2: xAt(selected), y1: plot.top, y2: plot.bottom })), lineSeries.map((entry, lineIndex) => (_jsx(Circle, { cx: xAt(selected), cy: PsChartScaleService.y(PsChartSeriesService.value(datum, entry.dataKey), domain, plot), fill: theme.surface, r: PsChartConfig.markerRadius, stroke: colors[lineIndexes[lineIndex]], strokeWidth: 2 }, entry.dataKey)))] }), _jsx(View, { ...panHandlers, style: StyleSheet.absoluteFill, testID: 'ps-chart-touch' })] })) }), showLegend && series.length > 1 ? (_jsx(PsChartLegend, { items: series.map((entry, position) => ({
81
+ }), index === null ? null : (_jsx(Line, { stroke: theme.muted, strokeWidth: PsChartConfig.scrubLineWidth, x1: xAt(selected), x2: xAt(selected), y1: plot.top, y2: plot.bottom })), lineSeries.map((entry, lineIndex) => (_jsx(Circle, { cx: xAt(selected), cy: PsChartScaleService.y(PsChartSeriesService.value(datum, entry.dataKey), domain, plot), fill: theme.surface, r: PsChartConfig.markerRadius, stroke: colors[lineIndexes[lineIndex]], strokeWidth: 2 }, entry.dataKey)))] }), _jsx(GestureDetector, { gesture: gesture, children: _jsx(View, { style: StyleSheet.absoluteFill, testID: 'ps-chart-touch' }) })] })) }), showLegend && series.length > 1 ? (_jsx(PsChartLegend, { items: series.map((entry, position) => ({
81
82
  color: colors[position],
82
83
  label: entry.label,
83
84
  })) })) : null] }));
@@ -11,5 +11,9 @@ export const PsChartFrame = ({ children, delta, period, title, value }) => {
11
11
  const size = value === undefined
12
12
  ? PsChartConfig.kpiValueFontMax
13
13
  : PsChartTextService.fontSize(value, width, PsChartConfig.kpiValueFontMax, PsChartConfig.kpiValueFontMin);
14
- return (_jsx(PsCard, { style: { gap: 12 }, children: _jsxs(View, { onLayout: onLayout, style: { gap: 12 }, testID: 'ps-chart-frame', children: [title === undefined && value === undefined ? null : (_jsxs(View, { style: { gap: 4 }, children: [title === undefined ? null : (_jsx(PsText, { numberOfLines: 1, tone: 'muted', variant: 'label', children: title })), value === undefined ? null : (_jsx(PsText, { numberOfLines: 1, style: { fontSize: size, lineHeight: Math.round(size * 1.25) }, variant: 'numeric', weight: 'bold', children: value })), delta === undefined ? null : _jsx(PsChartDeltaBadge, { ...delta }), period === undefined ? null : (_jsx(PsText, { tone: 'muted', variant: 'caption', children: period }))] })), children] }) }));
14
+ return (_jsx(PsCard, { style: { gap: 12 }, children: _jsxs(View, { onLayout: onLayout, style: { gap: 12 }, testID: 'ps-chart-frame', children: [title === undefined && value === undefined ? null : (_jsxs(View, { style: { gap: 4 }, children: [title === undefined ? null : (_jsx(PsText, { numberOfLines: 1, tone: 'muted', variant: 'label', children: title })), value === undefined ? null : (_jsx(PsText, { numberOfLines: 1, style: {
15
+ fontSize: size,
16
+ lineHeight: Math.round(size * 1.25),
17
+ opacity: width === 0 ? 0 : 1,
18
+ }, variant: 'numeric', weight: 'bold', children: value })), delta === undefined ? null : _jsx(PsChartDeltaBadge, { ...delta }), period === undefined ? null : (_jsx(PsText, { tone: 'muted', variant: 'caption', children: period }))] })), children] }) }));
15
19
  };
@@ -7,4 +7,5 @@ export interface PsDonutChartProps {
7
7
  formatShare: (share: number) => string;
8
8
  height?: number;
9
9
  isLoading?: boolean;
10
+ revealKey?: string;
10
11
  }
@@ -1,2 +1,2 @@
1
1
  import type { PsDonutChartProps } from './ps-donut-chart-props';
2
- export declare const PsDonutChart: ({ centerLabel, centerValue, data, emptyLabel, formatShare, height, isLoading, }: PsDonutChartProps) => import("react").JSX.Element;
2
+ export declare const PsDonutChart: ({ centerLabel, centerValue, data, emptyLabel, formatShare, height, isLoading, revealKey, }: PsDonutChartProps) => import("react").JSX.Element;
@@ -14,12 +14,12 @@ import { PsChartLegend } from '../ps-chart-legend/ps-chart-legend';
14
14
  import { PsChartSkeleton } from '../ps-chart-skeleton/ps-chart-skeleton';
15
15
  import { PsDonutSlice } from '../ps-donut-slice/ps-donut-slice';
16
16
  import { PsText } from '../ps-text/ps-text';
17
- export const PsDonutChart = ({ centerLabel, centerValue, data, emptyLabel, formatShare, height = PsChartConfig.donutHeight, isLoading = false, }) => {
17
+ export const PsDonutChart = ({ centerLabel, centerValue, data, emptyLabel, formatShare, height = PsChartConfig.donutHeight, isLoading = false, revealKey, }) => {
18
18
  const theme = usePsMobileTheme();
19
19
  const { onLayout, width } = usePsChartWidth();
20
20
  const fill = usePsWidgetFill();
21
21
  const [selected, setSelected] = useState(null);
22
- const progress = usePsChartReveal(data.map((datum) => `${datum.id}:${datum.share}`).join(','));
22
+ const progress = usePsChartReveal(!isLoading && width > 0, revealKey);
23
23
  if (isLoading)
24
24
  return _jsx(PsChartSkeleton, { height: height, variant: PsChartSkeletonVariant.Donut });
25
25
  const layout = PsChartDonutService.layout(width, height);
@@ -5,6 +5,8 @@ import { usePsHaptics } from '../../hooks/use-ps-haptics';
5
5
  import { PsActionMenuService } from '../../services/ps-action-menu-service';
6
6
  export const PsHeaderMenu = ({ iconKey = PsMobileIconKey.More, items, label, }) => {
7
7
  const haptic = usePsHaptics();
8
+ if (items.length === 0)
9
+ return null;
8
10
  const entries = PsActionMenuService.consistent([{ id: 'items', items }])[0];
9
11
  if (entries === undefined)
10
12
  return null;
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useState } from 'react';
3
3
  import { Modal, Pressable, ScrollView, StyleSheet, useWindowDimensions } from 'react-native';
4
4
  import Animated from 'react-native-reanimated';
5
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
5
6
  import { PsMenuPopupConfig } from '../../constants/ps-menu-popup-config';
6
7
  import { usePsMaterialScheme } from '../../hooks/use-ps-material-scheme';
7
8
  import { usePsMenuPopupReveal } from '../../hooks/use-ps-menu-popup-reveal';
@@ -12,34 +13,35 @@ export const PsMenuPopup = ({ anchor, children, isFullWidth = false, onClose, })
12
13
  const theme = usePsMobileTheme();
13
14
  const scheme = usePsMaterialScheme();
14
15
  const window = useWindowDimensions();
16
+ const insets = useSafeAreaInsets();
15
17
  const isMaterial = PsMenuPopupStyleService.isMaterial();
16
18
  const [size, setSize] = useState(undefined);
17
19
  const layout = size === undefined
18
- ? undefined
19
- : PsMenuPopupService.layout(anchor, window, size.height, size.width, isFullWidth);
20
+ ? PsMenuPopupService.layout(anchor, window, insets, Number.POSITIVE_INFINITY, PsMenuPopupConfig.minWidth, isFullWidth)
21
+ : PsMenuPopupService.layout(anchor, window, insets, size.height, size.width, isFullWidth);
20
22
  const container = PsMenuPopupStyleService.container(theme, scheme, isMaterial);
21
- const reveal = usePsMenuPopupReveal(layout !== undefined, layout === undefined || layout.isBelow, isMaterial);
23
+ const reveal = usePsMenuPopupReveal(size !== undefined, layout.isBelow, isMaterial);
22
24
  return (_jsxs(Modal, { animationType: isMaterial ? 'none' : 'fade', navigationBarTranslucent: true, onRequestClose: onClose, statusBarTranslucent: true, transparent: true, visible: true, children: [_jsx(Pressable, { accessibilityElementsHidden: true, onPress: onClose, style: StyleSheet.absoluteFill, testID: 'ps-menu-popup-backdrop' }), _jsx(Animated.View, { onLayout: (event) => {
23
25
  if (size === undefined)
24
26
  setSize({
25
27
  height: event.nativeEvent.layout.height,
26
28
  width: event.nativeEvent.layout.width,
27
29
  });
28
- }, pointerEvents: layout === undefined ? 'none' : 'auto', style: [
30
+ }, pointerEvents: size === undefined ? 'none' : 'auto', style: [
29
31
  {
30
32
  backgroundColor: container.backgroundColor,
31
33
  borderColor: container.borderColor,
32
34
  borderRadius: container.borderRadius,
33
35
  borderWidth: container.borderWidth,
34
36
  elevation: container.elevation,
35
- left: layout === undefined ? 0 : layout.left,
36
- maxHeight: layout === undefined ? undefined : layout.maxHeight,
37
+ left: layout.left,
38
+ maxHeight: layout.maxHeight,
37
39
  maxWidth: PsMenuPopupConfig.maxWidth,
38
40
  minWidth: isFullWidth ? undefined : PsMenuPopupConfig.minWidth,
39
41
  overflow: 'hidden',
40
42
  position: 'absolute',
41
- top: layout === undefined ? 0 : layout.top,
42
- width: layout === undefined ? undefined : layout.width,
43
+ top: layout.top,
44
+ width: layout.width,
43
45
  },
44
46
  reveal,
45
47
  ], testID: 'ps-menu-popup', children: _jsx(ScrollView, { bounces: false, contentContainerStyle: { paddingVertical: container.paddingVertical }, children: children }) })] }));
@@ -2,8 +2,11 @@ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useCallback, useEffect, useRef, useState } from 'react';
3
3
  import { BottomSheet, Group, RNHostView } from '@expo/ui/swift-ui';
4
4
  import { frame, interactiveDismissDisabled, presentationBackground, presentationDetents, presentationDragIndicator, } from '@expo/ui/swift-ui/modifiers';
5
+ import { useWindowDimensions } from 'react-native';
6
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
5
7
  import { PsNativeTreeContext } from '../../contexts/ps-native-tree-context';
6
8
  import { PsSheetMeasureContext } from '../../contexts/ps-sheet-measure-context';
9
+ import { usePsKeyboardHeight } from '../../hooks/use-ps-keyboard-height';
7
10
  import { usePsMobileTheme } from '../../hooks/use-ps-mobile-theme';
8
11
  import { PsSheetService } from '../../services/ps-sheet-service';
9
12
  import { PsHost } from '../ps-host/ps-host';
@@ -12,6 +15,10 @@ export const PsSheetPresenter = ({ children, isDismissBlocked, isOpen, onClose,
12
15
  const isOpenRef = useRef(isOpen);
13
16
  const isBlockedRef = useRef(isDismissBlocked);
14
17
  const [height, setHeight] = useState(0);
18
+ const keyboardHeight = usePsKeyboardHeight();
19
+ const window = useWindowDimensions();
20
+ const insets = useSafeAreaInsets();
21
+ const detentHeight = height > 0 ? PsSheetService.raisedHeight(height, keyboardHeight, window.height, insets.top) : 0;
15
22
  isOpenRef.current = isOpen;
16
23
  isBlockedRef.current = isDismissBlocked;
17
24
  const measure = useCallback((next) => {
@@ -27,7 +34,7 @@ export const PsSheetPresenter = ({ children, isDismissBlocked, isOpen, onClose,
27
34
  }
28
35
  }, children: _jsx(Group, { modifiers: [
29
36
  frame({ alignment: 'top', maxHeight: Infinity, maxWidth: Infinity }),
30
- presentationDetents(height > 0 ? [{ height }] : ['medium']),
37
+ presentationDetents(detentHeight > 0 ? [{ height: detentHeight }] : ['medium']),
31
38
  presentationDragIndicator('visible'),
32
39
  presentationBackground(theme.background),
33
40
  interactiveDismissDisabled(isDismissBlocked),
@@ -2,6 +2,7 @@ export interface PsSparklineProps {
2
2
  color?: string;
3
3
  height?: number;
4
4
  isLoading?: boolean;
5
+ revealKey?: string;
5
6
  showArea?: boolean;
6
7
  values: number[];
7
8
  }
@@ -1,2 +1,2 @@
1
1
  import type { PsSparklineProps } from './ps-sparkline-props';
2
- export declare const PsSparkline: ({ color, height: baseHeight, isLoading, showArea, values, }: PsSparklineProps) => import("react").JSX.Element;
2
+ export declare const PsSparkline: ({ color, height: baseHeight, isLoading, revealKey, showArea, values, }: PsSparklineProps) => import("react").JSX.Element;
@@ -10,11 +10,11 @@ import { usePsWidgetFill } from '../../hooks/use-ps-widget-fill';
10
10
  import { PsChartScaleService } from '../../services/ps-chart-scale-service';
11
11
  import { PsChartLine } from '../ps-chart-line/ps-chart-line';
12
12
  import { PsChartSkeleton } from '../ps-chart-skeleton/ps-chart-skeleton';
13
- export const PsSparkline = ({ color, height: baseHeight = PsChartConfig.sparklineHeight, isLoading = false, showArea = true, values, }) => {
13
+ export const PsSparkline = ({ color, height: baseHeight = PsChartConfig.sparklineHeight, isLoading = false, revealKey, showArea = true, values, }) => {
14
14
  const theme = usePsMobileTheme();
15
15
  const height = baseHeight + usePsWidgetFill();
16
16
  const { onLayout, width } = usePsChartWidth();
17
- const progress = usePsChartReveal(values.join(','));
17
+ const progress = usePsChartReveal(!isLoading && width > 0, revealKey);
18
18
  if (isLoading)
19
19
  return _jsx(PsChartSkeleton, { height: height, variant: PsChartSkeletonVariant.Line });
20
20
  if (values.length === 0)
@@ -6,4 +6,5 @@ export interface PsWaterfallChartProps {
6
6
  formatValue: (value: number) => string;
7
7
  height?: number;
8
8
  isLoading?: boolean;
9
+ revealKey?: string;
9
10
  }
@@ -1,2 +1,2 @@
1
1
  import type { PsWaterfallChartProps } from './ps-waterfall-chart-props';
2
- export declare const PsWaterfallChart: ({ data, emptyLabel, formatTick, formatValue, height: baseHeight, isLoading, }: PsWaterfallChartProps) => import("react").JSX.Element;
2
+ export declare const PsWaterfallChart: ({ data, emptyLabel, formatTick, formatValue, height: baseHeight, isLoading, revealKey, }: PsWaterfallChartProps) => import("react").JSX.Element;
@@ -1,5 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { StyleSheet, View } from 'react-native';
3
+ import { GestureDetector } from 'react-native-gesture-handler';
3
4
  import Svg, { G, Line, Text as SvgText } from 'react-native-svg';
4
5
  import { PsChartConfig } from '../../constants/ps-chart-config';
5
6
  import { PsChartSkeletonVariant } from '../../constants/ps-chart-skeleton-variant';
@@ -16,7 +17,7 @@ import { PsChartBar } from '../ps-chart-bar/ps-chart-bar';
16
17
  import { PsChartEmptyState } from '../ps-chart-empty-state/ps-chart-empty-state';
17
18
  import { PsChartReadout } from '../ps-chart-readout/ps-chart-readout';
18
19
  import { PsChartSkeleton } from '../ps-chart-skeleton/ps-chart-skeleton';
19
- export const PsWaterfallChart = ({ data, emptyLabel, formatTick, formatValue, height: baseHeight = PsChartConfig.chartHeight, isLoading = false, }) => {
20
+ export const PsWaterfallChart = ({ data, emptyLabel, formatTick, formatValue, height: baseHeight = PsChartConfig.chartHeight, isLoading = false, revealKey, }) => {
20
21
  const theme = usePsMobileTheme();
21
22
  const { onLayout, width } = usePsChartWidth();
22
23
  const height = baseHeight + usePsWidgetFill();
@@ -24,8 +25,8 @@ export const PsWaterfallChart = ({ data, emptyLabel, formatTick, formatValue, he
24
25
  const domain = PsChartScaleService.domain(PsWaterfallService.values(steps));
25
26
  const tickLabels = PsChartScaleService.ticks(domain).map((tick) => formatTick(tick));
26
27
  const plot = PsChartScaleService.plot(width, height, PsChartAxisService.yAxisWidth(tickLabels, width));
27
- const { index, panHandlers } = usePsChartScrub(data.length, true, plot);
28
- const progress = usePsChartReveal(`waterfall:${PsWaterfallService.values(steps).join(',')}`);
28
+ const { gesture, index } = usePsChartScrub(data.length, true, plot);
29
+ const progress = usePsChartReveal(!isLoading && width > 0, revealKey);
29
30
  if (isLoading)
30
31
  return _jsx(PsChartSkeleton, { height: baseHeight, variant: PsChartSkeletonVariant.Bars });
31
32
  if (steps.length === 0 || PsWaterfallService.values(steps).every((value) => value === 0))
@@ -36,8 +37,8 @@ export const PsWaterfallChart = ({ data, emptyLabel, formatTick, formatValue, he
36
37
  const step = steps[selected];
37
38
  const rects = PsWaterfallService.rects(steps, colors, domain, plot);
38
39
  const color = step.isTotal ? colors[2] : step.value < 0 ? colors[1] : colors[0];
39
- return (_jsxs(View, { style: { gap: 8 }, children: [_jsx(PsChartReadout, { items: [{ color, label: step.label, value: formatValue(step.value) }], title: step.label }), _jsx(View, { accessible: true, accessibilityLabel: `${step.label}, ${formatValue(step.value)}`, accessibilityRole: 'image', onLayout: onLayout, style: { height }, children: width === 0 ? null : (_jsxs(_Fragment, { children: [_jsxs(Svg, { height: height, pointerEvents: 'none', width: width, children: [PsChartScaleService.ticks(domain).map((tick, tickIndex) => {
40
+ return (_jsxs(View, { style: { gap: 8 }, children: [_jsx(PsChartReadout, { items: [{ color, label: step.label, value: formatValue(step.value) }], title: step.label }), _jsx(View, { accessible: true, accessibilityLabel: `${step.label}, ${formatValue(step.value)}`, accessibilityRole: 'image', onLayout: onLayout, style: { height }, children: width === 0 ? (_jsx(PsChartSkeleton, { height: baseHeight, variant: PsChartSkeletonVariant.Bars })) : (_jsxs(_Fragment, { children: [_jsxs(Svg, { height: height, pointerEvents: 'none', width: width, children: [PsChartScaleService.ticks(domain).map((tick, tickIndex) => {
40
41
  const y = PsChartScaleService.y(tick, domain, plot);
41
42
  return (_jsxs(G, { children: [_jsx(Line, { stroke: theme.line, strokeWidth: PsChartConfig.gridLineWidth, x1: plot.left, x2: plot.right, y1: y, y2: y }), _jsx(SvgText, { fill: theme.muted, fontFamily: theme.fonts.medium, fontSize: PsChartConfig.axisLabelSize, textAnchor: 'end', x: plot.left - PsChartConfig.axisTickGap, y: y + PsChartConfig.axisLabelSize / 3, children: PsChartTextService.fit(tickLabels[tickIndex], plot.left - PsChartConfig.axisTickGap, PsChartConfig.axisLabelSize) })] }, tick));
42
- }), xLabels.indexes.map((position) => (_jsx(SvgText, { fill: theme.muted, fontFamily: theme.fonts.medium, fontSize: PsChartConfig.axisLabelSize, textAnchor: 'middle', x: PsChartScaleService.categoryX(position, steps.length, plot, true), y: height - 6, children: PsChartTextService.fit(steps[position].label, xLabels.maxWidth, PsChartConfig.axisLabelSize) }, steps[position].label))), rects.map((rect) => (_jsx(PsChartBar, { progress: progress, rect: rect }, rect.id)))] }), _jsx(View, { ...panHandlers, style: StyleSheet.absoluteFill, testID: 'ps-chart-touch' })] })) })] }));
43
+ }), xLabels.indexes.map((position) => (_jsx(SvgText, { fill: theme.muted, fontFamily: theme.fonts.medium, fontSize: PsChartConfig.axisLabelSize, textAnchor: 'middle', x: PsChartScaleService.categoryX(position, steps.length, plot, true), y: height - 6, children: PsChartTextService.fit(steps[position].label, xLabels.maxWidth, PsChartConfig.axisLabelSize) }, steps[position].label))), rects.map((rect) => (_jsx(PsChartBar, { progress: progress, rect: rect }, rect.id)))] }), _jsx(GestureDetector, { gesture: gesture, children: _jsx(View, { style: StyleSheet.absoluteFill, testID: 'ps-chart-touch' }) })] })) })] }));
43
44
  };
@@ -45,7 +45,10 @@ export declare const PsChartConfig: {
45
45
  readonly readoutLineHeight: 22;
46
46
  readonly readoutRowGap: 2;
47
47
  readonly readoutValueSize: 15;
48
+ readonly scrubActiveOffset: 8;
49
+ readonly scrubFailOffsetY: 10;
48
50
  readonly scrubLineWidth: 1;
51
+ readonly scrubTestId: "ps-chart-scrub";
49
52
  readonly skeletonBarHeights: readonly [0.45, 0.7, 0.55, 0.85, 0.65, 0.95, 0.75];
50
53
  readonly skeletonPulseMs: 900;
51
54
  readonly sparklineHeight: 40;
@@ -45,7 +45,10 @@ export const PsChartConfig = {
45
45
  readoutLineHeight: 22,
46
46
  readoutRowGap: 2,
47
47
  readoutValueSize: 15,
48
+ scrubActiveOffset: 8,
49
+ scrubFailOffsetY: 10,
48
50
  scrubLineWidth: 1,
51
+ scrubTestId: 'ps-chart-scrub',
49
52
  skeletonBarHeights: [0.45, 0.7, 0.55, 0.85, 0.65, 0.95, 0.75],
50
53
  skeletonPulseMs: 900,
51
54
  sparklineHeight: 40,
@@ -1,2 +1,2 @@
1
1
  import { type SharedValue } from 'react-native-reanimated';
2
- export declare const usePsChartReveal: (signature: string) => SharedValue<number>;
2
+ export declare const usePsChartReveal: (isReady: boolean, revealKey: string | undefined) => SharedValue<number>;
@@ -2,7 +2,7 @@ import { useEffect } from 'react';
2
2
  import { useSharedValue, withTiming } from 'react-native-reanimated';
3
3
  import { PsChartConfig } from '../constants/ps-chart-config';
4
4
  import { usePsMobileTheme } from './use-ps-mobile-theme';
5
- export const usePsChartReveal = (signature) => {
5
+ export const usePsChartReveal = (isReady, revealKey) => {
6
6
  const { isReduceMotion: isReduced } = usePsMobileTheme();
7
7
  const progress = useSharedValue(isReduced ? 1 : 0);
8
8
  useEffect(() => {
@@ -10,8 +10,10 @@ export const usePsChartReveal = (signature) => {
10
10
  progress.set(1);
11
11
  return;
12
12
  }
13
+ if (!isReady)
14
+ return;
13
15
  progress.set(0);
14
16
  progress.set(withTiming(1, { duration: PsChartConfig.drawMs }));
15
- }, [isReduced, progress, signature]);
17
+ }, [isReady, isReduced, progress, revealKey]);
16
18
  return progress;
17
19
  };
@@ -1,23 +1,22 @@
1
- import { useMemo, useState } from 'react';
2
- import { PanResponder } from 'react-native';
1
+ import { useState } from 'react';
2
+ import { usePanGesture } from 'react-native-gesture-handler';
3
+ import { PsChartConfig } from '../constants/ps-chart-config';
3
4
  import { PsChartScrubService } from '../services/ps-chart-scrub-service';
4
5
  export const usePsChartScrub = (count, isBand, plot) => {
5
6
  const [index, setIndex] = useState(null);
6
7
  const { bottom, left, right, top } = plot;
7
- const panHandlers = useMemo(() => {
8
- const area = { bottom, left, right, top };
9
- const find = (x) => isBand
10
- ? PsChartScrubService.bandIndex(x, area, count)
11
- : PsChartScrubService.pointIndex(x, area, count);
12
- return PanResponder.create({
13
- onMoveShouldSetPanResponder: (_event, gesture) => Math.abs(gesture.dx) > Math.abs(gesture.dy),
14
- onPanResponderGrant: (event) => setIndex(find(event.nativeEvent.locationX)),
15
- onPanResponderMove: (event) => setIndex(find(event.nativeEvent.locationX)),
16
- onPanResponderRelease: () => setIndex(null),
17
- onPanResponderTerminate: () => setIndex(null),
18
- onPanResponderTerminationRequest: () => true,
19
- onStartShouldSetPanResponder: () => true,
20
- }).panHandlers;
21
- }, [bottom, count, isBand, left, right, top]);
22
- return { index, panHandlers };
8
+ const area = { bottom, left, right, top };
9
+ const find = (x) => isBand
10
+ ? PsChartScrubService.bandIndex(x, area, count)
11
+ : PsChartScrubService.pointIndex(x, area, count);
12
+ const gesture = usePanGesture({
13
+ activeOffsetX: [-PsChartConfig.scrubActiveOffset, PsChartConfig.scrubActiveOffset],
14
+ failOffsetY: [-PsChartConfig.scrubFailOffsetY, PsChartConfig.scrubFailOffsetY],
15
+ onActivate: (event) => setIndex(find(event.x)),
16
+ onFinalize: () => setIndex(null),
17
+ onUpdate: (event) => setIndex(find(event.x)),
18
+ runOnJS: true,
19
+ testID: PsChartConfig.scrubTestId,
20
+ });
21
+ return { gesture, index };
23
22
  };
@@ -1,3 +1,3 @@
1
1
  import type { PsInfiniteDataOptions } from '../interfaces/ps-infinite-data-options';
2
2
  import type { PsInfiniteDataResult } from '../interfaces/ps-infinite-data-result';
3
- export declare const usePsInfiniteData: <T>({ getKey, isEnabled, key, loadPage, onSettled, onWriteFailed, refreshKey, }: PsInfiniteDataOptions<T>) => PsInfiniteDataResult<T>;
3
+ export declare const usePsInfiniteData: <T>({ getKey, isEnabled, key, keepPreviousOnKeyChange, loadPage, onSettled, onWriteFailed, refreshKey, }: PsInfiniteDataOptions<T>) => PsInfiniteDataResult<T>;
@@ -1,17 +1,25 @@
1
1
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
2
  import { usePsChangeQueue, usePsOptimisticRows } from '@pallasoft/ps-utilities/react';
3
3
  import { PsInfiniteDataService } from '../services/ps-infinite-data-service';
4
- export const usePsInfiniteData = ({ getKey, isEnabled = true, key, loadPage, onSettled, onWriteFailed, refreshKey, }) => {
4
+ export const usePsInfiniteData = ({ getKey, isEnabled = true, key, keepPreviousOnKeyChange = false, loadPage, onSettled, onWriteFailed, refreshKey, }) => {
5
5
  const [state, setState] = useState(() => PsInfiniteDataService.initial());
6
6
  const [isSaving, setIsSaving] = useState(false);
7
+ const [isRefreshing, setIsRefreshing] = useState(false);
7
8
  const isSavingRef = useRef(false);
8
9
  const submittedRef = useRef(0);
9
10
  const stateRef = useRef(state);
10
11
  const committedRef = useRef([]);
11
12
  const versionRef = useRef(0);
12
- const latestRef = useRef({ getKey, loadPage, onSettled, onWriteFailed });
13
+ const latestRef = useRef({
14
+ getKey,
15
+ keepPreviousOnKeyChange,
16
+ loadPage,
17
+ onSettled,
18
+ onWriteFailed,
19
+ });
20
+ const loadedKeyRef = useRef(key);
13
21
  useEffect(() => {
14
- latestRef.current = { getKey, loadPage, onSettled, onWriteFailed };
22
+ latestRef.current = { getKey, keepPreviousOnKeyChange, loadPage, onSettled, onWriteFailed };
15
23
  });
16
24
  const commit = useCallback((next) => {
17
25
  stateRef.current = next(stateRef.current);
@@ -21,15 +29,17 @@ export const usePsInfiniteData = ({ getKey, isEnabled = true, key, loadPage, onS
21
29
  isSavingRef.current = value;
22
30
  setIsSaving(value);
23
31
  }, []);
24
- const fetchFirst = useCallback(async (isSilent) => {
32
+ const fetchFirst = useCallback(async (isSilent, isTracked) => {
25
33
  versionRef.current += 1;
26
34
  const version = versionRef.current;
27
35
  if (!isSilent)
28
36
  commit(() => PsInfiniteDataService.initial());
37
+ setIsRefreshing(isTracked);
29
38
  try {
30
39
  const page = await latestRef.current.loadPage(null);
31
40
  if (version !== versionRef.current)
32
41
  return;
42
+ setIsRefreshing(false);
33
43
  commit(() => ({
34
44
  error: false,
35
45
  isLoading: false,
@@ -42,6 +52,7 @@ export const usePsInfiniteData = ({ getKey, isEnabled = true, key, loadPage, onS
42
52
  catch {
43
53
  if (version !== versionRef.current)
44
54
  return;
55
+ setIsRefreshing(false);
45
56
  commit((current) => ({ ...current, error: true, isLoading: false, isLoadingMore: false }));
46
57
  }
47
58
  }, [commit]);
@@ -83,10 +94,14 @@ export const usePsInfiniteData = ({ getKey, isEnabled = true, key, loadPage, onS
83
94
  setSaving(false);
84
95
  if (!isEnabled) {
85
96
  versionRef.current += 1;
97
+ setIsRefreshing(false);
86
98
  commit(() => PsInfiniteDataService.initial());
87
99
  return undefined;
88
100
  }
89
- void fetchFirst(false);
101
+ const isKept = stateRef.current.items.length > 0 &&
102
+ PsInfiniteDataService.keepsPrevious(latestRef.current.keepPreviousOnKeyChange, loadedKeyRef.current, key);
103
+ loadedKeyRef.current = key;
104
+ void fetchFirst(isKept, isKept);
90
105
  return () => {
91
106
  versionRef.current += 1;
92
107
  };
@@ -125,7 +140,7 @@ export const usePsInfiniteData = ({ getKey, isEnabled = true, key, loadPage, onS
125
140
  commit((value) => ({ ...value, error: true, isLoadingMore: false }));
126
141
  }
127
142
  }, [commit]);
128
- const refresh = useCallback(() => fetchFirst(true), [fetchFirst]);
143
+ const refresh = useCallback(() => fetchFirst(true, false), [fetchFirst]);
129
144
  const apply = useCallback((rows, updates) => PsInfiniteDataService.apply(rows, updates), []);
130
145
  const optimistic = usePsOptimisticRows({
131
146
  apply,
@@ -184,9 +199,10 @@ export const usePsInfiniteData = ({ getKey, isEnabled = true, key, loadPage, onS
184
199
  hasMore: PsInfiniteDataService.hasMore(state),
185
200
  isLoading: state.isLoading,
186
201
  isLoadingMore: state.isLoadingMore,
202
+ isRefreshing,
187
203
  items: optimistic.rows,
188
204
  loadMore,
189
205
  refresh,
190
206
  write,
191
- }), [loadMore, optimistic.rows, refresh, state, write]);
207
+ }), [isRefreshing, loadMore, optimistic.rows, refresh, state, write]);
192
208
  };
@@ -0,0 +1 @@
1
+ export declare const usePsKeyboardHeight: () => number;
@@ -0,0 +1,14 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { Keyboard } from 'react-native';
3
+ export const usePsKeyboardHeight = () => {
4
+ const [height, setHeight] = useState(0);
5
+ useEffect(() => {
6
+ const show = Keyboard.addListener('keyboardWillShow', (event) => setHeight(event.endCoordinates.height));
7
+ const hide = Keyboard.addListener('keyboardWillHide', () => setHeight(0));
8
+ return () => {
9
+ show.remove();
10
+ hide.remove();
11
+ };
12
+ }, []);
13
+ return height;
14
+ };
@@ -1,5 +1,5 @@
1
- import type { GestureResponderHandlers } from 'react-native';
1
+ import type { PanGesture } from 'react-native-gesture-handler';
2
2
  export interface PsChartScrubResult {
3
+ gesture: PanGesture;
3
4
  index: number | null;
4
- panHandlers: GestureResponderHandlers;
5
5
  }
@@ -2,6 +2,7 @@ import type { PsInfinitePage } from './ps-infinite-page';
2
2
  export interface PsInfiniteDataOptions<T> {
3
3
  getKey: (item: T) => string;
4
4
  isEnabled?: boolean;
5
+ keepPreviousOnKeyChange?: boolean | ((previousKey: string, nextKey: string) => boolean);
5
6
  key: string;
6
7
  loadPage: (cursor: string | null) => Promise<PsInfinitePage<T>>;
7
8
  onSettled?: () => Promise<unknown>;
@@ -5,6 +5,7 @@ export interface PsInfiniteDataResult<T> {
5
5
  hasMore: boolean;
6
6
  isLoading: boolean;
7
7
  isLoadingMore: boolean;
8
+ isRefreshing: boolean;
8
9
  items: T[];
9
10
  loadMore: () => Promise<void>;
10
11
  refresh: () => Promise<void>;
@@ -0,0 +1,4 @@
1
+ export interface PsMenuPopupInsets {
2
+ bottom: number;
3
+ top: number;
4
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -7,5 +7,4 @@ export declare class PsChartSeriesService {
7
7
  static stackedValues(data: PsAreaChartDatum[], series: PsCartesianChartSeries[]): number[];
8
8
  static color({ count, explicit, index, theme }: PsChartPaletteInput): string;
9
9
  static isEmpty(data: PsAreaChartDatum[], series: PsCartesianChartSeries[]): boolean;
10
- static signature(kind: string, data: PsAreaChartDatum[], series: PsCartesianChartSeries[]): string;
11
10
  }
@@ -28,7 +28,4 @@ export class PsChartSeriesService {
28
28
  static isEmpty(data, series) {
29
29
  return PsChartSeriesService.values(data, series).every((value) => value === 0);
30
30
  }
31
- static signature(kind, data, series) {
32
- return `${kind}:${PsChartSeriesService.values(data, series).join(',')}`;
33
- }
34
31
  }
@@ -4,5 +4,6 @@ export declare class PsInfiniteDataService {
4
4
  static initial<T>(): PsInfiniteDataState<T>;
5
5
  static merge<T>(existing: T[], incoming: T[], getKey: (item: T) => string): T[];
6
6
  static apply<T>(rows: T[], updates: PsInfiniteDataUpdate<T>[]): T[];
7
+ static keepsPrevious(option: boolean | ((previousKey: string, nextKey: string) => boolean), previousKey: string, nextKey: string): boolean;
7
8
  static hasMore<T>(state: PsInfiniteDataState<T>): boolean;
8
9
  }
@@ -16,6 +16,9 @@ export class PsInfiniteDataService {
16
16
  static apply(rows, updates) {
17
17
  return updates.reduce((current, update) => update(current), rows);
18
18
  }
19
+ static keepsPrevious(option, previousKey, nextKey) {
20
+ return typeof option === 'boolean' ? option : option(previousKey, nextKey);
21
+ }
19
22
  static hasMore(state) {
20
23
  return state.nextCursor !== null;
21
24
  }
@@ -1,6 +1,7 @@
1
1
  import type { PsMenuAnchor } from '../interfaces/ps-menu-anchor';
2
+ import type { PsMenuPopupInsets } from '../interfaces/ps-menu-popup-insets';
2
3
  import type { PsMenuPopupLayout } from '../interfaces/ps-menu-popup-layout';
3
4
  import type { PsMenuPopupWindow } from '../interfaces/ps-menu-popup-window';
4
5
  export declare class PsMenuPopupService {
5
- static layout(anchor: PsMenuAnchor, window: PsMenuPopupWindow, contentHeight: number, contentWidth: number, isFullWidth: boolean): PsMenuPopupLayout;
6
+ static layout(anchor: PsMenuAnchor, window: PsMenuPopupWindow, insets: PsMenuPopupInsets, contentHeight: number, contentWidth: number, isFullWidth: boolean): PsMenuPopupLayout;
6
7
  }
@@ -1,12 +1,12 @@
1
1
  import { PsMenuPopupConfig } from '../constants/ps-menu-popup-config';
2
2
  export class PsMenuPopupService {
3
- static layout(anchor, window, contentHeight, contentWidth, isFullWidth) {
3
+ static layout(anchor, window, insets, contentHeight, contentWidth, isFullWidth) {
4
4
  const margin = PsMenuPopupConfig.edgeMargin;
5
5
  const gap = PsMenuPopupConfig.anchorGap;
6
6
  const width = isFullWidth ? anchor.width : undefined;
7
7
  const menuWidth = width === undefined ? contentWidth : width;
8
- const below = window.height - (anchor.y + anchor.height) - gap - margin;
9
- const above = anchor.y - gap - margin;
8
+ const below = window.height - (anchor.y + anchor.height) - gap - Math.max(margin, insets.bottom);
9
+ const above = anchor.y - gap - Math.max(margin, insets.top);
10
10
  const isBelow = contentHeight <= below || below >= above;
11
11
  const room = isBelow ? below : above;
12
12
  const height = Math.min(contentHeight, Math.max(room, 0));
@@ -6,6 +6,7 @@ export declare class PsSheetService {
6
6
  static handlesInset(os: string): boolean;
7
7
  static bottomPadding(isFloating: boolean, bottomInset: number): number;
8
8
  static fittedHeight(headerHeight: number, bodyHeight: number, windowHeight: number): number;
9
+ static raisedHeight(height: number, keyboardHeight: number, windowHeight: number, topInset: number): number;
9
10
  static closeAction(label: string | undefined, onPress: () => void): PsSheetAction | undefined;
10
11
  static isActionInactive(action: PsSheetAction): boolean;
11
12
  static actionOpacity(action: PsSheetAction, isPressed: boolean): number;
@@ -13,6 +13,9 @@ export class PsSheetService {
13
13
  static fittedHeight(headerHeight, bodyHeight, windowHeight) {
14
14
  return Math.min(Math.ceil(headerHeight + bodyHeight), PsSheetService.maxHeight(windowHeight));
15
15
  }
16
+ static raisedHeight(height, keyboardHeight, windowHeight, topInset) {
17
+ return Math.min(height + keyboardHeight, windowHeight - topInset);
18
+ }
16
19
  static closeAction(label, onPress) {
17
20
  return label === undefined ? undefined : { label, onPress };
18
21
  }
package/jest-setup.js CHANGED
@@ -103,6 +103,10 @@ jest.mock('expo-router', () => {
103
103
  Stack.Toolbar.Button = ({ children, disabled, onPress }) =>
104
104
  React.createElement(View, { accessibilityLabel: children, disabled, onPress });
105
105
  Stack.Toolbar.Spacer = () => null;
106
+ Stack.Toolbar.Menu = ({ accessibilityLabel, children }) =>
107
+ React.createElement(View, { accessibilityLabel, testID: 'stack-toolbar-menu' }, children);
108
+ Stack.Toolbar.MenuAction = ({ children, onPress }) =>
109
+ React.createElement(View, { accessibilityLabel: children, onPress });
106
110
 
107
111
  const ThemeProvider = ({ children }) => React.createElement(React.Fragment, null, children);
108
112
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pallasoft/ps-mobile-components",
3
- "version": "1.10.1",
3
+ "version": "1.11.0",
4
4
  "private": false,
5
5
  "description": "Native iOS (SwiftUI) and Android (Jetpack Compose) Ps components for Pallasoft mobile apps",
6
6
  "main": "./dist/index.js",