@pallasoft/ps-mobile-components 1.5.0 → 1.7.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 (82) hide show
  1. package/dist/components/ps-infinite-list/index.d.ts +2 -0
  2. package/dist/components/ps-infinite-list/index.js +2 -0
  3. package/dist/components/ps-infinite-list/ps-infinite-list-footer.d.ts +2 -0
  4. package/dist/components/ps-infinite-list/ps-infinite-list-footer.js +5 -0
  5. package/dist/components/ps-infinite-list/ps-infinite-list-item.d.ts +2 -0
  6. package/dist/components/ps-infinite-list/ps-infinite-list-item.js +8 -0
  7. package/dist/components/ps-infinite-list/ps-infinite-list-props.d.ts +34 -0
  8. package/dist/components/ps-infinite-list/ps-infinite-list-props.js +1 -0
  9. package/dist/components/ps-infinite-list/ps-infinite-list.d.ts +2 -0
  10. package/dist/components/ps-infinite-list/ps-infinite-list.js +39 -0
  11. package/dist/components/ps-list/ps-list-select-row.js +8 -4
  12. package/dist/components/ps-menu-popup/ps-menu-popup-divider.js +8 -2
  13. package/dist/components/ps-menu-popup/ps-menu-popup-row.js +21 -9
  14. package/dist/components/ps-menu-popup/ps-menu-popup.js +28 -17
  15. package/dist/constants/ps-infinite-config.d.ts +6 -0
  16. package/dist/constants/ps-infinite-config.js +6 -0
  17. package/dist/constants/ps-material-menu-config.d.ts +12 -0
  18. package/dist/constants/ps-material-menu-config.js +12 -0
  19. package/dist/constants/ps-scan-format.d.ts +4 -0
  20. package/dist/constants/ps-scan-format.js +5 -0
  21. package/dist/constants/ps-scan-output-config.d.ts +17 -0
  22. package/dist/constants/ps-scan-output-config.js +23 -0
  23. package/dist/hooks/use-ps-document-scanner.js +3 -2
  24. package/dist/hooks/use-ps-infinite-data.d.ts +3 -0
  25. package/dist/hooks/use-ps-infinite-data.js +192 -0
  26. package/dist/hooks/use-ps-menu-popup-reveal.d.ts +3 -0
  27. package/dist/hooks/use-ps-menu-popup-reveal.js +28 -0
  28. package/dist/index.d.ts +8 -0
  29. package/dist/index.js +8 -0
  30. package/dist/interfaces/ps-capture-options.d.ts +2 -0
  31. package/dist/interfaces/ps-document-scanner-options.d.ts +2 -0
  32. package/dist/interfaces/ps-image-manipulator-native.d.ts +19 -0
  33. package/dist/interfaces/ps-image-manipulator-native.js +1 -0
  34. package/dist/interfaces/ps-infinite-data-options.d.ts +10 -0
  35. package/dist/interfaces/ps-infinite-data-options.js +1 -0
  36. package/dist/interfaces/ps-infinite-data-result.d.ts +12 -0
  37. package/dist/interfaces/ps-infinite-data-result.js +1 -0
  38. package/dist/interfaces/ps-infinite-data-state.d.ts +8 -0
  39. package/dist/interfaces/ps-infinite-data-state.js +1 -0
  40. package/dist/interfaces/ps-infinite-data-update.d.ts +1 -0
  41. package/dist/interfaces/ps-infinite-data-update.js +1 -0
  42. package/dist/interfaces/ps-infinite-data-write.d.ts +6 -0
  43. package/dist/interfaces/ps-infinite-data-write.js +1 -0
  44. package/dist/interfaces/ps-infinite-page.d.ts +4 -0
  45. package/dist/interfaces/ps-infinite-page.js +1 -0
  46. package/dist/interfaces/ps-menu-popup-container-style.d.ts +8 -0
  47. package/dist/interfaces/ps-menu-popup-container-style.js +1 -0
  48. package/dist/interfaces/ps-menu-popup-divider-style.d.ts +4 -0
  49. package/dist/interfaces/ps-menu-popup-divider-style.js +1 -0
  50. package/dist/interfaces/ps-menu-popup-layout.d.ts +1 -0
  51. package/dist/interfaces/ps-menu-popup-row-style.d.ts +6 -0
  52. package/dist/interfaces/ps-menu-popup-row-style.js +1 -0
  53. package/dist/interfaces/ps-menu-popup-size.d.ts +4 -0
  54. package/dist/interfaces/ps-menu-popup-size.js +1 -0
  55. package/dist/interfaces/ps-page-size.d.ts +4 -0
  56. package/dist/interfaces/ps-page-size.js +1 -0
  57. package/dist/interfaces/ps-pdf-page.d.ts +5 -0
  58. package/dist/interfaces/ps-pdf-page.js +1 -0
  59. package/dist/interfaces/ps-scan-format-labels.d.ts +7 -0
  60. package/dist/interfaces/ps-scan-format-labels.js +1 -0
  61. package/dist/services/ps-capture-picker-service.js +1 -0
  62. package/dist/services/ps-document-scanner-service.d.ts +2 -0
  63. package/dist/services/ps-document-scanner-service.js +21 -4
  64. package/dist/services/ps-infinite-data-service.d.ts +8 -0
  65. package/dist/services/ps-infinite-data-service.js +22 -0
  66. package/dist/services/ps-infinite-list-service.d.ts +10 -0
  67. package/dist/services/ps-infinite-list-service.js +36 -0
  68. package/dist/services/ps-jpeg-size-service.d.ts +4 -0
  69. package/dist/services/ps-jpeg-size-service.js +32 -0
  70. package/dist/services/ps-menu-popup-service.js +1 -0
  71. package/dist/services/ps-menu-popup-style-service.d.ts +13 -0
  72. package/dist/services/ps-menu-popup-style-service.js +55 -0
  73. package/dist/services/ps-scan-format-prompt-service.d.ts +6 -0
  74. package/dist/services/ps-scan-format-prompt-service.js +21 -0
  75. package/dist/services/ps-scan-image-converter-service.d.ts +5 -0
  76. package/dist/services/ps-scan-image-converter-service.js +22 -0
  77. package/dist/services/ps-scan-output-service.d.ts +10 -0
  78. package/dist/services/ps-scan-output-service.js +55 -0
  79. package/dist/services/ps-scan-pdf-service.d.ts +4 -0
  80. package/dist/services/ps-scan-pdf-service.js +6 -0
  81. package/jest-setup.js +1 -1
  82. package/package.json +6 -2
@@ -0,0 +1,2 @@
1
+ export * from './ps-infinite-list';
2
+ export * from './ps-infinite-list-props';
@@ -0,0 +1,2 @@
1
+ export * from './ps-infinite-list';
2
+ export * from './ps-infinite-list-props';
@@ -0,0 +1,2 @@
1
+ import type { PsInfiniteListFooterProps } from './ps-infinite-list-props';
2
+ export declare const PsInfiniteListFooter: ({ isLoadingMore }: PsInfiniteListFooterProps) => import("react").JSX.Element | null;
@@ -0,0 +1,5 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { View } from 'react-native';
3
+ import { PsLayoutConfig } from '../../constants/ps-layout-config';
4
+ import { PsSpinner } from '../ps-spinner/ps-spinner';
5
+ export const PsInfiniteListFooter = ({ isLoadingMore }) => isLoadingMore ? (_jsx(View, { style: { alignItems: 'center', paddingVertical: PsLayoutConfig.gutter }, testID: 'ps-infinite-list-loading-more', children: _jsx(PsSpinner, {}) })) : null;
@@ -0,0 +1,2 @@
1
+ import type { PsInfiniteListItemProps } from './ps-infinite-list-props';
2
+ export declare const PsInfiniteListItem: ({ children, count, index }: PsInfiniteListItemProps) => import("react").JSX.Element;
@@ -0,0 +1,8 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { View } from 'react-native';
3
+ import { usePsMobileTheme } from '../../hooks/use-ps-mobile-theme';
4
+ import { PsInfiniteListService } from '../../services/ps-infinite-list-service';
5
+ export const PsInfiniteListItem = ({ children, count, index }) => {
6
+ const theme = usePsMobileTheme();
7
+ return (_jsxs(View, { style: PsInfiniteListService.itemFrame(theme, index, count), children: [index === 0 ? null : _jsx(View, { style: PsInfiniteListService.divider(theme) }), children] }));
8
+ };
@@ -0,0 +1,34 @@
1
+ import type { ReactElement, ReactNode } from 'react';
2
+ import type { StyleProp, ViewStyle } from 'react-native';
3
+ import type { PsListVariant } from '../../services/ps-list-service';
4
+ import type { PsListSkeletonLayout } from '../ps-list/ps-list-props';
5
+ export interface PsInfiniteSection<T> {
6
+ data: T[];
7
+ footer?: string;
8
+ key: string;
9
+ title?: string;
10
+ }
11
+ export interface PsInfiniteListProps<T> {
12
+ empty?: ReactNode;
13
+ hasMore: boolean;
14
+ header?: ReactNode;
15
+ isLoading?: boolean;
16
+ isLoadingMore?: boolean;
17
+ items?: T[];
18
+ keyExtractor: (item: T) => string;
19
+ onEndReached: () => void;
20
+ onRefresh?: () => Promise<void>;
21
+ renderItem: (item: T) => ReactElement;
22
+ sections?: PsInfiniteSection<T>[];
23
+ skeleton?: PsListSkeletonLayout;
24
+ style?: StyleProp<ViewStyle>;
25
+ variant?: PsListVariant;
26
+ }
27
+ export interface PsInfiniteListFooterProps {
28
+ isLoadingMore: boolean;
29
+ }
30
+ export interface PsInfiniteListItemProps {
31
+ children: ReactElement;
32
+ count: number;
33
+ index: number;
34
+ }
@@ -0,0 +1,2 @@
1
+ import type { PsInfiniteListProps } from './ps-infinite-list-props';
2
+ export declare const PsInfiniteList: <T>({ empty, hasMore, header, isLoading, isLoadingMore, items, keyExtractor, onEndReached, onRefresh, renderItem, sections, skeleton, style, variant, }: PsInfiniteListProps<T>) => import("react").JSX.Element;
@@ -0,0 +1,39 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Platform, RefreshControl, SectionList, View } from 'react-native';
3
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
4
+ import { PsInfiniteConfig } from '../../constants/ps-infinite-config';
5
+ import { PsLayoutConfig } from '../../constants/ps-layout-config';
6
+ import { PsSelectionBarConfig } from '../../constants/ps-selection-bar-config';
7
+ import { PsScrollContext } from '../../contexts/ps-scroll-context';
8
+ import { usePsMobileTheme } from '../../hooks/use-ps-mobile-theme';
9
+ import { usePsRefresh } from '../../hooks/use-ps-refresh';
10
+ import { usePsScrollHost } from '../../hooks/use-ps-scroll-host';
11
+ import { usePsTabBar } from '../../hooks/use-ps-tab-bar';
12
+ import { usePsTopInset } from '../../hooks/use-ps-top-inset';
13
+ import { PsInfiniteListService } from '../../services/ps-infinite-list-service';
14
+ import { PsListService } from '../../services/ps-list-service';
15
+ import { PsScreenService } from '../../services/ps-screen-service';
16
+ import { PsSkeletonList } from '../ps-skeleton/ps-skeleton-list';
17
+ import { PsText } from '../ps-text/ps-text';
18
+ import { PsInfiniteListFooter } from './ps-infinite-list-footer';
19
+ import { PsInfiniteListItem } from './ps-infinite-list-item';
20
+ export const PsInfiniteList = ({ empty, hasMore, header, isLoading = false, isLoadingMore = false, items, keyExtractor, onEndReached, onRefresh, renderItem, sections, skeleton, style, variant = 'grouped', }) => {
21
+ const theme = usePsMobileTheme();
22
+ const insets = useSafeAreaInsets();
23
+ const topInset = usePsTopInset();
24
+ const { hasTabBar, isHidden: isTabBarHidden } = usePsTabBar();
25
+ const { isRefreshing, refresh } = usePsRefresh(onRefresh);
26
+ const host = usePsScrollHost();
27
+ const shown = isLoading ? [] : PsInfiniteListService.sections(items, sections);
28
+ return (_jsxs(View, { style: { flex: 1 }, children: [_jsx(PsScrollContext, { value: host.context, children: _jsx(SectionList, { ListEmptyComponent: isLoading ? _jsx(PsSkeletonList, { layout: skeleton }) : _jsx(_Fragment, { children: empty }), ListFooterComponent: _jsx(PsInfiniteListFooter, { isLoadingMore: isLoadingMore }), ListHeaderComponent: header === undefined ? undefined : (_jsx(View, { style: { marginBottom: PsLayoutConfig.listGap }, children: header })), contentContainerStyle: PsScreenService.content({
29
+ bottom: PsScreenService.bottomInset(insets.bottom, hasTabBar, isTabBarHidden),
30
+ top: topInset,
31
+ }, Platform.OS === 'ios', PsListService.hasGutter(variant), 0, isTabBarHidden ? PsSelectionBarConfig.height : 0), contentInsetAdjustmentBehavior: 'automatic', initialNumToRender: PsInfiniteConfig.initialNumToRender, keyExtractor: keyExtractor, keyboardDismissMode: 'interactive', keyboardShouldPersistTaps: 'handled', maxToRenderPerBatch: PsInfiniteConfig.maxToRenderPerBatch, onContentSizeChange: host.onContentSizeChange, onEndReached: () => {
32
+ if (PsInfiniteListService.shouldLoadMore(hasMore, isLoading, isLoadingMore))
33
+ onEndReached();
34
+ }, onEndReachedThreshold: PsInfiniteConfig.endReachedThreshold, onLayout: host.onLayout, onScroll: host.onScroll, refreshControl: onRefresh === undefined ? undefined : (_jsx(RefreshControl, { colors: [theme.primaryText], onRefresh: () => void refresh(), progressBackgroundColor: theme.surface, refreshing: isRefreshing, tintColor: theme.primary })), renderItem: ({ index, item, section }) => (_jsx(PsInfiniteListItem, { count: section.data.length, index: index, children: renderItem(item) })), renderSectionFooter: ({ section }) => (_jsx(View, { style: {
35
+ gap: PsLayoutConfig.listSectionGap,
36
+ paddingBottom: section === shown[shown.length - 1] ? 0 : PsLayoutConfig.listGap,
37
+ paddingTop: section.footer === undefined ? 0 : PsLayoutConfig.listSectionGap,
38
+ }, children: section.footer === undefined ? null : (_jsx(PsText, { tone: 'muted', variant: 'caption', children: section.footer })) })), renderSectionHeader: ({ section }) => section.title === undefined ? null : (_jsx(View, { style: { paddingBottom: PsLayoutConfig.listSectionGap }, children: _jsx(PsText, { accessibilityRole: 'header', tone: 'muted', variant: 'label', children: section.title }) })), scrollEnabled: !host.isLocked, scrollEventThrottle: PsLayoutConfig.scrollEventThrottle, sections: shown, showsVerticalScrollIndicator: false, stickySectionHeadersEnabled: false, style: [{ backgroundColor: theme.background, flex: 1 }, style], testID: 'ps-infinite-list', windowSize: PsInfiniteConfig.windowSize }) }), header === undefined ? null : (_jsx(View, { pointerEvents: 'none', style: [PsScreenService.statusBarScrim(topInset), { backgroundColor: theme.background }], testID: 'ps-infinite-list-status-bar-scrim' }))] }));
39
+ };
@@ -1,10 +1,12 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { PsIconKey } from '@pallasoft/ps-icons';
3
- import { View } from 'react-native';
3
+ import { Pressable, View } from 'react-native';
4
4
  import { PsLayoutConfig } from '../../constants/ps-layout-config';
5
5
  import { PsMobileControlConfig } from '../../constants/ps-mobile-control-config';
6
+ import { PsRippleKind } from '../../constants/ps-ripple-kind';
6
7
  import { usePsMobileTheme } from '../../hooks/use-ps-mobile-theme';
7
8
  import { PsNativeMenuService } from '../../services/ps-native-menu-service';
9
+ import { PsRippleService } from '../../services/ps-ripple-service';
8
10
  import { PsSelectService } from '../../services/ps-select-service';
9
11
  import { PsStateService } from '../../services/ps-state-service';
10
12
  import { PsIcon } from '../ps-icon/ps-icon';
@@ -15,14 +17,16 @@ export const PsListSelectRow = ({ isDisabled = false, isLoading = false, onChang
15
17
  const theme = usePsMobileTheme();
16
18
  if (isLoading)
17
19
  return _jsx(PsListRowSkeleton, { subtitle: subtitle, value: value });
18
- return (_jsx(PsNativeMenu, { isDisabled: isDisabled, isFullWidth: true, items: PsNativeMenuService.items(options, value), label: title, onSelect: onChange, children: () => (_jsxs(View, { accessibilityRole: 'button', accessibilityValue: { text: PsSelectService.selectedLabel(options, value) }, style: {
20
+ return (_jsx(PsNativeMenu, { isDisabled: isDisabled, isFullWidth: true, items: PsNativeMenuService.items(options, value), label: title, onSelect: onChange, children: ({ open }) => (_jsxs(Pressable, { accessibilityLabel: title, accessibilityRole: 'button', accessibilityState: { disabled: isDisabled }, accessibilityValue: { text: PsSelectService.selectedLabel(options, value) }, android_ripple: PsRippleService.resolve(theme, PsRippleKind.Surface), disabled: isDisabled, onPress: open, style: ({ pressed }) => ({
19
21
  alignItems: 'center',
20
- backgroundColor: theme.surface,
22
+ backgroundColor: pressed
23
+ ? PsStateService.pressedFill(theme, theme.surface)
24
+ : theme.surface,
21
25
  flexDirection: 'row',
22
26
  gap: PsLayoutConfig.headerSlotGap,
23
27
  minHeight: PsLayoutConfig.rowMinHeight,
24
28
  opacity: isDisabled ? PsStateService.disabledOpacity() : 1,
25
29
  paddingHorizontal: PsLayoutConfig.gutter,
26
30
  paddingVertical: PsLayoutConfig.rowPaddingVertical,
27
- }, testID: 'ps-list-select-row', children: [_jsxs(View, { style: { flex: 1, gap: PsLayoutConfig.rowLineGap }, children: [_jsx(PsText, { numberOfLines: 1, weight: 'semibold', children: title }), subtitle === undefined ? null : (_jsx(PsText, { tone: 'muted', variant: 'small', children: subtitle }))] }), valueAccessory, _jsx(PsText, { numberOfLines: 1, style: { maxWidth: PsLayoutConfig.rowValueMaxWidth }, tone: 'muted', children: PsSelectService.selectedLabel(options, value) }), _jsx(PsIcon, { color: theme.muted, iconKey: PsIconKey.Sort, size: PsMobileControlConfig.iconSizeInline })] })) }));
31
+ }), testID: 'ps-list-select-row', children: [_jsxs(View, { style: { flex: 1, gap: PsLayoutConfig.rowLineGap }, children: [_jsx(PsText, { numberOfLines: 1, weight: 'semibold', children: title }), subtitle === undefined ? null : (_jsx(PsText, { tone: 'muted', variant: 'small', children: subtitle }))] }), valueAccessory, _jsx(PsText, { numberOfLines: 1, style: { maxWidth: PsLayoutConfig.rowValueMaxWidth }, tone: 'muted', children: PsSelectService.selectedLabel(options, value) }), _jsx(PsIcon, { color: theme.muted, iconKey: PsIconKey.Sort, size: PsMobileControlConfig.iconSizeInline })] })) }));
28
32
  };
@@ -1,7 +1,13 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { View } from 'react-native';
3
+ import { usePsMaterialScheme } from '../../hooks/use-ps-material-scheme';
3
4
  import { usePsMobileTheme } from '../../hooks/use-ps-mobile-theme';
5
+ import { PsMenuPopupStyleService } from '../../services/ps-menu-popup-style-service';
4
6
  export const PsMenuPopupDivider = () => {
5
- const theme = usePsMobileTheme();
6
- return _jsx(View, { style: { backgroundColor: theme.line, height: 1 } });
7
+ const divider = PsMenuPopupStyleService.divider(usePsMobileTheme(), usePsMaterialScheme(), PsMenuPopupStyleService.isMaterial());
8
+ return (_jsx(View, { style: {
9
+ backgroundColor: divider.backgroundColor,
10
+ height: 1,
11
+ marginVertical: divider.marginVertical,
12
+ } }));
7
13
  };
@@ -3,20 +3,32 @@ import { PsIconKey } from '@pallasoft/ps-icons';
3
3
  import { Pressable, View } from 'react-native';
4
4
  import { PsMenuPopupConfig } from '../../constants/ps-menu-popup-config';
5
5
  import { PsMobileControlConfig } from '../../constants/ps-mobile-control-config';
6
+ import { PsRippleKind } from '../../constants/ps-ripple-kind';
7
+ import { usePsMaterialScheme } from '../../hooks/use-ps-material-scheme';
6
8
  import { usePsMobileTheme } from '../../hooks/use-ps-mobile-theme';
9
+ import { PsMenuPopupStyleService } from '../../services/ps-menu-popup-style-service';
10
+ import { PsRippleService } from '../../services/ps-ripple-service';
7
11
  import { PsStateService } from '../../services/ps-state-service';
8
12
  import { PsIcon } from '../ps-icon/ps-icon';
9
13
  import { PsText } from '../ps-text/ps-text';
10
14
  export const PsMenuPopupRow = ({ iconKey, isChecked = false, isDanger = false, isDisabled = false, isWarning = false, label, onPress, subtitle, }) => {
11
15
  const theme = usePsMobileTheme();
16
+ const scheme = usePsMaterialScheme();
17
+ const isMaterial = PsMenuPopupStyleService.isMaterial();
12
18
  const tint = isDanger ? theme.danger : theme.ink;
13
- return (_jsxs(Pressable, { accessibilityRole: 'menuitem', accessibilityState: { disabled: isDisabled, selected: isChecked }, disabled: isDisabled, onPress: onPress, style: ({ pressed }) => ({
14
- alignItems: 'center',
15
- backgroundColor: pressed ? theme.primarySoft : 'transparent',
16
- flexDirection: 'row',
17
- gap: PsMobileControlConfig.rowSpacing,
18
- minHeight: PsMenuPopupConfig.rowHeight,
19
- opacity: isDisabled ? PsStateService.disabledOpacity() : 1,
20
- paddingHorizontal: PsMenuPopupConfig.rowPaddingHorizontal,
21
- }), children: [iconKey === undefined ? null : _jsx(PsIcon, { color: tint, iconKey: iconKey }), _jsxs(View, { style: { flex: 1 }, children: [_jsx(PsText, { numberOfLines: 1, tone: isDanger ? 'danger' : 'ink', weight: isChecked ? 'semibold' : 'regular', children: label }), subtitle === undefined ? null : (_jsx(PsText, { numberOfLines: 1, tone: isWarning ? 'warning' : 'muted', variant: 'small', children: subtitle }))] }), isChecked ? _jsx(PsIcon, { color: theme.primaryText, iconKey: PsIconKey.Check }) : null] }));
19
+ return (_jsxs(Pressable, { accessibilityRole: 'menuitem', accessibilityState: { disabled: isDisabled, selected: isChecked }, android_ripple: PsRippleService.resolve(theme, isDanger ? PsRippleKind.Danger : PsRippleKind.Surface), disabled: isDisabled, onPress: onPress, style: ({ pressed }) => {
20
+ const row = PsMenuPopupStyleService.row(theme, scheme, isMaterial, isChecked, pressed);
21
+ return {
22
+ alignItems: 'center',
23
+ backgroundColor: row.backgroundColor,
24
+ borderRadius: row.borderRadius,
25
+ flexDirection: 'row',
26
+ gap: PsMobileControlConfig.rowSpacing,
27
+ marginHorizontal: row.marginHorizontal,
28
+ minHeight: PsMenuPopupConfig.rowHeight,
29
+ opacity: isDisabled ? PsStateService.disabledOpacity() : 1,
30
+ overflow: 'hidden',
31
+ paddingHorizontal: row.paddingHorizontal,
32
+ };
33
+ }, children: [iconKey === undefined ? null : _jsx(PsIcon, { color: tint, iconKey: iconKey }), _jsxs(View, { style: { flex: 1 }, children: [_jsx(PsText, { numberOfLines: 1, tone: isDanger ? 'danger' : 'ink', weight: PsMenuPopupStyleService.labelWeight(isMaterial, isChecked), children: label }), subtitle === undefined ? null : (_jsx(PsText, { numberOfLines: 1, tone: isWarning ? 'warning' : 'muted', variant: 'small', children: subtitle }))] }), isChecked ? _jsx(PsIcon, { color: theme.primaryText, iconKey: PsIconKey.Check }) : null] }));
22
34
  };
@@ -1,35 +1,46 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useState } from 'react';
3
- import { Modal, Pressable, ScrollView, StyleSheet, View, useWindowDimensions } from 'react-native';
3
+ import { Modal, Pressable, ScrollView, StyleSheet, useWindowDimensions } from 'react-native';
4
+ import Animated from 'react-native-reanimated';
4
5
  import { PsMenuPopupConfig } from '../../constants/ps-menu-popup-config';
6
+ import { usePsMaterialScheme } from '../../hooks/use-ps-material-scheme';
7
+ import { usePsMenuPopupReveal } from '../../hooks/use-ps-menu-popup-reveal';
5
8
  import { usePsMobileTheme } from '../../hooks/use-ps-mobile-theme';
6
9
  import { PsMenuPopupService } from '../../services/ps-menu-popup-service';
10
+ import { PsMenuPopupStyleService } from '../../services/ps-menu-popup-style-service';
7
11
  export const PsMenuPopup = ({ anchor, children, isFullWidth = false, onClose, }) => {
8
12
  const theme = usePsMobileTheme();
13
+ const scheme = usePsMaterialScheme();
9
14
  const window = useWindowDimensions();
15
+ const isMaterial = PsMenuPopupStyleService.isMaterial();
10
16
  const [size, setSize] = useState(undefined);
11
17
  const layout = size === undefined
12
18
  ? undefined
13
19
  : PsMenuPopupService.layout(anchor, window, size.height, size.width, isFullWidth);
14
- return (_jsxs(Modal, { animationType: '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(View, { onLayout: (event) => {
20
+ const container = PsMenuPopupStyleService.container(theme, scheme, isMaterial);
21
+ const reveal = usePsMenuPopupReveal(layout !== undefined, layout === undefined || layout.isBelow, isMaterial);
22
+ 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) => {
15
23
  if (size === undefined)
16
24
  setSize({
17
25
  height: event.nativeEvent.layout.height,
18
26
  width: event.nativeEvent.layout.width,
19
27
  });
20
- }, pointerEvents: layout === undefined ? 'none' : 'auto', style: {
21
- backgroundColor: theme.surface,
22
- borderColor: theme.line,
23
- borderRadius: PsMenuPopupConfig.radius,
24
- borderWidth: 1,
25
- left: layout === undefined ? 0 : layout.left,
26
- maxHeight: layout === undefined ? undefined : layout.maxHeight,
27
- maxWidth: PsMenuPopupConfig.maxWidth,
28
- minWidth: isFullWidth ? undefined : PsMenuPopupConfig.minWidth,
29
- opacity: layout === undefined ? 0 : 1,
30
- overflow: 'hidden',
31
- position: 'absolute',
32
- top: layout === undefined ? 0 : layout.top,
33
- width: layout === undefined ? undefined : layout.width,
34
- }, testID: 'ps-menu-popup', children: _jsx(ScrollView, { bounces: false, contentContainerStyle: { paddingVertical: PsMenuPopupConfig.paddingVertical }, children: children }) })] }));
28
+ }, pointerEvents: layout === undefined ? 'none' : 'auto', style: [
29
+ {
30
+ backgroundColor: container.backgroundColor,
31
+ borderColor: container.borderColor,
32
+ borderRadius: container.borderRadius,
33
+ borderWidth: container.borderWidth,
34
+ elevation: container.elevation,
35
+ left: layout === undefined ? 0 : layout.left,
36
+ maxHeight: layout === undefined ? undefined : layout.maxHeight,
37
+ maxWidth: PsMenuPopupConfig.maxWidth,
38
+ minWidth: isFullWidth ? undefined : PsMenuPopupConfig.minWidth,
39
+ overflow: 'hidden',
40
+ position: 'absolute',
41
+ top: layout === undefined ? 0 : layout.top,
42
+ width: layout === undefined ? undefined : layout.width,
43
+ },
44
+ reveal,
45
+ ], testID: 'ps-menu-popup', children: _jsx(ScrollView, { bounces: false, contentContainerStyle: { paddingVertical: container.paddingVertical }, children: children }) })] }));
35
46
  };
@@ -0,0 +1,6 @@
1
+ export declare const PsInfiniteConfig: {
2
+ readonly endReachedThreshold: 1.5;
3
+ readonly initialNumToRender: 12;
4
+ readonly maxToRenderPerBatch: 12;
5
+ readonly windowSize: 9;
6
+ };
@@ -0,0 +1,6 @@
1
+ export const PsInfiniteConfig = {
2
+ endReachedThreshold: 1.5,
3
+ initialNumToRender: 12,
4
+ maxToRenderPerBatch: 12,
5
+ windowSize: 9,
6
+ };
@@ -0,0 +1,12 @@
1
+ export declare const PsMaterialMenuConfig: {
2
+ readonly dividerInset: 8;
3
+ readonly elevation: 3;
4
+ readonly enterEasing: readonly [0.05, 0.7, 0.1, 1];
5
+ readonly enterDurationMs: 150;
6
+ readonly enterScale: 0.8;
7
+ readonly itemInset: 4;
8
+ readonly itemRadius: 12;
9
+ readonly paddingVertical: 4;
10
+ readonly radius: 16;
11
+ readonly rowPaddingHorizontal: 12;
12
+ };
@@ -0,0 +1,12 @@
1
+ export const PsMaterialMenuConfig = {
2
+ dividerInset: 8,
3
+ elevation: 3,
4
+ enterEasing: [0.05, 0.7, 0.1, 1],
5
+ enterDurationMs: 150,
6
+ enterScale: 0.8,
7
+ itemInset: 4,
8
+ itemRadius: 12,
9
+ paddingVertical: 4,
10
+ radius: 16,
11
+ rowPaddingHorizontal: 12,
12
+ };
@@ -0,0 +1,4 @@
1
+ export declare enum PsScanFormat {
2
+ Pdf = "pdf",
3
+ Png = "png"
4
+ }
@@ -0,0 +1,5 @@
1
+ export var PsScanFormat;
2
+ (function (PsScanFormat) {
3
+ PsScanFormat["Pdf"] = "pdf";
4
+ PsScanFormat["Png"] = "png";
5
+ })(PsScanFormat || (PsScanFormat = {}));
@@ -0,0 +1,17 @@
1
+ import { PsScanFormat } from './ps-scan-format';
2
+ export declare const PsScanOutputConfig: {
3
+ readonly extensions: Record<PsScanFormat, string>;
4
+ readonly jpegMarkerPrefix: 255;
5
+ readonly jpegSizeMarkers: readonly [192, 193, 194, 195, 197, 198, 199, 201, 202, 203, 205, 206, 207];
6
+ readonly jpegStartMarker: 216;
7
+ readonly jpegSegmentHeaderLength: 2;
8
+ readonly jpegSizeOffset: 3;
9
+ readonly pdfBinaryComment: readonly [37, 226, 227, 207, 211, 10];
10
+ readonly pdfLongEdgePoints: 842;
11
+ readonly pdfObjectsPerPage: 3;
12
+ readonly pdfPageObjectBase: 3;
13
+ readonly pdfType: "application/pdf";
14
+ readonly pngType: "image/png";
15
+ readonly promptDelayMs: 400;
16
+ readonly types: Record<PsScanFormat, string>;
17
+ };
@@ -0,0 +1,23 @@
1
+ import { PsScanFormat } from './ps-scan-format';
2
+ export const PsScanOutputConfig = {
3
+ extensions: {
4
+ [PsScanFormat.Pdf]: 'pdf',
5
+ [PsScanFormat.Png]: 'png',
6
+ },
7
+ jpegMarkerPrefix: 0xff,
8
+ jpegSizeMarkers: [0xc0, 0xc1, 0xc2, 0xc3, 0xc5, 0xc6, 0xc7, 0xc9, 0xca, 0xcb, 0xcd, 0xce, 0xcf],
9
+ jpegStartMarker: 0xd8,
10
+ jpegSegmentHeaderLength: 2,
11
+ jpegSizeOffset: 3,
12
+ pdfBinaryComment: [0x25, 0xe2, 0xe3, 0xcf, 0xd3, 0x0a],
13
+ pdfLongEdgePoints: 842,
14
+ pdfObjectsPerPage: 3,
15
+ pdfPageObjectBase: 3,
16
+ pdfType: 'application/pdf',
17
+ pngType: 'image/png',
18
+ promptDelayMs: 400,
19
+ types: {
20
+ [PsScanFormat.Pdf]: 'application/pdf',
21
+ [PsScanFormat.Png]: 'image/png',
22
+ },
23
+ };
@@ -3,12 +3,13 @@ import { PsDocumentScannerService } from '../services/ps-document-scanner-servic
3
3
  export const usePsDocumentScanner = (defaults = {}) => {
4
4
  const [isScanning, setIsScanning] = useState(false);
5
5
  const isAvailable = useMemo(() => PsDocumentScannerService.isAvailable(), []);
6
- const { fileName, jpegQuality, maxPages } = defaults;
6
+ const { fileName, formatLabels, jpegQuality, maxPages } = defaults;
7
7
  const scan = useCallback(async (options = {}) => {
8
8
  setIsScanning(true);
9
9
  try {
10
10
  return await PsDocumentScannerService.scan({
11
11
  fileName: options.fileName === undefined ? fileName : options.fileName,
12
+ formatLabels: options.formatLabels === undefined ? formatLabels : options.formatLabels,
12
13
  jpegQuality: options.jpegQuality === undefined ? jpegQuality : options.jpegQuality,
13
14
  maxPages: options.maxPages === undefined ? maxPages : options.maxPages,
14
15
  });
@@ -16,6 +17,6 @@ export const usePsDocumentScanner = (defaults = {}) => {
16
17
  finally {
17
18
  setIsScanning(false);
18
19
  }
19
- }, [fileName, jpegQuality, maxPages]);
20
+ }, [fileName, formatLabels, jpegQuality, maxPages]);
20
21
  return { isAvailable, isScanning, scan };
21
22
  };
@@ -0,0 +1,3 @@
1
+ import type { PsInfiniteDataOptions } from '../interfaces/ps-infinite-data-options';
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>;
@@ -0,0 +1,192 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
+ import { usePsChangeQueue, usePsOptimisticRows } from '@pallasoft/ps-utilities/react';
3
+ import { PsInfiniteDataService } from '../services/ps-infinite-data-service';
4
+ export const usePsInfiniteData = ({ getKey, isEnabled = true, key, loadPage, onSettled, onWriteFailed, refreshKey, }) => {
5
+ const [state, setState] = useState(() => PsInfiniteDataService.initial());
6
+ const [isSaving, setIsSaving] = useState(false);
7
+ const isSavingRef = useRef(false);
8
+ const submittedRef = useRef(0);
9
+ const stateRef = useRef(state);
10
+ const committedRef = useRef([]);
11
+ const versionRef = useRef(0);
12
+ const latestRef = useRef({ getKey, loadPage, onSettled, onWriteFailed });
13
+ useEffect(() => {
14
+ latestRef.current = { getKey, loadPage, onSettled, onWriteFailed };
15
+ });
16
+ const commit = useCallback((next) => {
17
+ stateRef.current = next(stateRef.current);
18
+ setState(stateRef.current);
19
+ }, []);
20
+ const setSaving = useCallback((value) => {
21
+ isSavingRef.current = value;
22
+ setIsSaving(value);
23
+ }, []);
24
+ const fetchFirst = useCallback(async (isSilent) => {
25
+ versionRef.current += 1;
26
+ const version = versionRef.current;
27
+ if (!isSilent)
28
+ commit(() => PsInfiniteDataService.initial());
29
+ try {
30
+ const page = await latestRef.current.loadPage(null);
31
+ if (version !== versionRef.current)
32
+ return;
33
+ commit(() => ({
34
+ error: false,
35
+ isLoading: false,
36
+ isLoadingMore: false,
37
+ items: page.items,
38
+ nextCursor: page.nextCursor,
39
+ pages: 1,
40
+ }));
41
+ }
42
+ catch {
43
+ if (version !== versionRef.current)
44
+ return;
45
+ commit((current) => ({ ...current, error: true, isLoading: false, isLoadingMore: false }));
46
+ }
47
+ }, [commit]);
48
+ const reloadLoaded = useCallback(async () => {
49
+ versionRef.current += 1;
50
+ const version = versionRef.current;
51
+ const target = Math.max(stateRef.current.pages, 1);
52
+ try {
53
+ let items = [];
54
+ let cursor = null;
55
+ let pages = 0;
56
+ while (pages < target) {
57
+ const page = await latestRef.current.loadPage(cursor);
58
+ if (version !== versionRef.current)
59
+ return;
60
+ items = PsInfiniteDataService.merge(items, page.items, latestRef.current.getKey);
61
+ cursor = page.nextCursor;
62
+ pages += 1;
63
+ if (cursor === null)
64
+ break;
65
+ }
66
+ commit(() => ({
67
+ error: false,
68
+ isLoading: false,
69
+ isLoadingMore: false,
70
+ items,
71
+ nextCursor: cursor,
72
+ pages,
73
+ }));
74
+ }
75
+ catch {
76
+ if (version !== versionRef.current)
77
+ return;
78
+ commit((current) => ({ ...current, error: true }));
79
+ }
80
+ }, [commit]);
81
+ useEffect(() => {
82
+ committedRef.current = [];
83
+ setSaving(false);
84
+ if (!isEnabled) {
85
+ versionRef.current += 1;
86
+ commit(() => PsInfiniteDataService.initial());
87
+ return undefined;
88
+ }
89
+ void fetchFirst(false);
90
+ return () => {
91
+ versionRef.current += 1;
92
+ };
93
+ }, [key, isEnabled, fetchFirst, commit, setSaving]);
94
+ const refreshRef = useRef(refreshKey);
95
+ useEffect(() => {
96
+ if (refreshRef.current === refreshKey)
97
+ return;
98
+ refreshRef.current = refreshKey;
99
+ if (isEnabled && !isSavingRef.current && !stateRef.current.isLoading)
100
+ void reloadLoaded();
101
+ }, [isEnabled, refreshKey, reloadLoaded]);
102
+ const loadMore = useCallback(async () => {
103
+ const current = stateRef.current;
104
+ if (current.isLoading || current.isLoadingMore || current.nextCursor === null)
105
+ return;
106
+ const version = versionRef.current;
107
+ const cursor = current.nextCursor;
108
+ commit((value) => ({ ...value, isLoadingMore: true }));
109
+ try {
110
+ const page = await latestRef.current.loadPage(cursor);
111
+ if (version !== versionRef.current)
112
+ return;
113
+ commit((value) => ({
114
+ ...value,
115
+ error: false,
116
+ isLoadingMore: false,
117
+ items: PsInfiniteDataService.merge(value.items, page.items, latestRef.current.getKey),
118
+ nextCursor: page.nextCursor,
119
+ pages: value.pages + 1,
120
+ }));
121
+ }
122
+ catch {
123
+ if (version !== versionRef.current)
124
+ return;
125
+ commit((value) => ({ ...value, error: true, isLoadingMore: false }));
126
+ }
127
+ }, [commit]);
128
+ const refresh = useCallback(() => fetchFirst(true), [fetchFirst]);
129
+ const apply = useCallback((rows, updates) => PsInfiniteDataService.apply(rows, updates), []);
130
+ const optimistic = usePsOptimisticRows({
131
+ apply,
132
+ isSaving,
133
+ rows: state.items,
134
+ });
135
+ const rememberRef = useRef(optimistic.remember);
136
+ useEffect(() => {
137
+ rememberRef.current = optimistic.remember;
138
+ });
139
+ const send = useCallback(async (writes) => {
140
+ for (const item of writes) {
141
+ await item.request();
142
+ committedRef.current = [...committedRef.current, item];
143
+ item.resolve(true);
144
+ }
145
+ }, []);
146
+ const onFailed = useCallback((failed, dropped) => {
147
+ for (const item of [...failed, ...dropped]) {
148
+ optimistic.forgetLast();
149
+ item.resolve(false);
150
+ }
151
+ if (latestRef.current.onWriteFailed !== undefined)
152
+ latestRef.current.onWriteFailed();
153
+ }, [optimistic.forgetLast]);
154
+ const onDrained = useCallback(() => {
155
+ const through = submittedRef.current;
156
+ const settled = committedRef.current;
157
+ committedRef.current = [];
158
+ void (async () => {
159
+ const settle = latestRef.current.onSettled;
160
+ if (settle !== undefined)
161
+ await settle().catch(() => undefined);
162
+ if (settled.length > 0)
163
+ commit((current) => ({
164
+ ...current,
165
+ items: PsInfiniteDataService.apply(current.items, settled.map((item) => item.update)),
166
+ }));
167
+ if (submittedRef.current === through)
168
+ setSaving(false);
169
+ })();
170
+ }, [commit, setSaving]);
171
+ const queue = usePsChangeQueue({ onDrained, onFailed, send });
172
+ const { enqueue } = queue;
173
+ const write = useCallback((update, request) => {
174
+ if (stateRef.current.isLoading)
175
+ return Promise.resolve(false);
176
+ submittedRef.current += 1;
177
+ setSaving(true);
178
+ rememberRef.current(update);
179
+ return new Promise((resolve) => enqueue({ request, resolve, update }));
180
+ }, [enqueue, setSaving]);
181
+ return useMemo(() => ({
182
+ committedItems: state.items,
183
+ error: state.error,
184
+ hasMore: PsInfiniteDataService.hasMore(state),
185
+ isLoading: state.isLoading,
186
+ isLoadingMore: state.isLoadingMore,
187
+ items: optimistic.rows,
188
+ loadMore,
189
+ refresh,
190
+ write,
191
+ }), [loadMore, optimistic.rows, refresh, state, write]);
192
+ };
@@ -0,0 +1,3 @@
1
+ import type { ViewStyle } from 'react-native';
2
+ import { type AnimatedStyle } from 'react-native-reanimated';
3
+ export declare const usePsMenuPopupReveal: (isPlaced: boolean, isBelow: boolean, isMaterial: boolean) => AnimatedStyle<ViewStyle>;
@@ -0,0 +1,28 @@
1
+ import { useEffect } from 'react';
2
+ import { Easing, interpolate, useAnimatedStyle, useSharedValue, withTiming, } from 'react-native-reanimated';
3
+ import { PsMaterialMenuConfig } from '../constants/ps-material-menu-config';
4
+ import { usePsMobileTheme } from './use-ps-mobile-theme';
5
+ export const usePsMenuPopupReveal = (isPlaced, isBelow, isMaterial) => {
6
+ const { isReduceMotion } = usePsMobileTheme();
7
+ const isInstant = isReduceMotion || !isMaterial;
8
+ const progress = useSharedValue(isInstant ? 1 : 0);
9
+ useEffect(() => {
10
+ if (!isPlaced)
11
+ return;
12
+ if (isInstant) {
13
+ progress.set(1);
14
+ return;
15
+ }
16
+ progress.set(withTiming(1, {
17
+ duration: PsMaterialMenuConfig.enterDurationMs,
18
+ easing: Easing.bezier(...PsMaterialMenuConfig.enterEasing),
19
+ }));
20
+ }, [isInstant, isPlaced, progress]);
21
+ return useAnimatedStyle(() => ({
22
+ opacity: isPlaced ? progress.get() : 0,
23
+ transform: [
24
+ { scaleY: interpolate(progress.get(), [0, 1], [PsMaterialMenuConfig.enterScale, 1]) },
25
+ ],
26
+ transformOrigin: isBelow ? 'top' : 'bottom',
27
+ }));
28
+ };