@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.
- package/dist/components/ps-infinite-list/index.d.ts +2 -0
- package/dist/components/ps-infinite-list/index.js +2 -0
- package/dist/components/ps-infinite-list/ps-infinite-list-footer.d.ts +2 -0
- package/dist/components/ps-infinite-list/ps-infinite-list-footer.js +5 -0
- package/dist/components/ps-infinite-list/ps-infinite-list-item.d.ts +2 -0
- package/dist/components/ps-infinite-list/ps-infinite-list-item.js +8 -0
- package/dist/components/ps-infinite-list/ps-infinite-list-props.d.ts +34 -0
- package/dist/components/ps-infinite-list/ps-infinite-list-props.js +1 -0
- package/dist/components/ps-infinite-list/ps-infinite-list.d.ts +2 -0
- package/dist/components/ps-infinite-list/ps-infinite-list.js +39 -0
- package/dist/components/ps-list/ps-list-select-row.js +8 -4
- package/dist/components/ps-menu-popup/ps-menu-popup-divider.js +8 -2
- package/dist/components/ps-menu-popup/ps-menu-popup-row.js +21 -9
- package/dist/components/ps-menu-popup/ps-menu-popup.js +28 -17
- package/dist/constants/ps-infinite-config.d.ts +6 -0
- package/dist/constants/ps-infinite-config.js +6 -0
- package/dist/constants/ps-material-menu-config.d.ts +12 -0
- package/dist/constants/ps-material-menu-config.js +12 -0
- package/dist/constants/ps-scan-format.d.ts +4 -0
- package/dist/constants/ps-scan-format.js +5 -0
- package/dist/constants/ps-scan-output-config.d.ts +17 -0
- package/dist/constants/ps-scan-output-config.js +23 -0
- package/dist/hooks/use-ps-document-scanner.js +3 -2
- package/dist/hooks/use-ps-infinite-data.d.ts +3 -0
- package/dist/hooks/use-ps-infinite-data.js +192 -0
- package/dist/hooks/use-ps-menu-popup-reveal.d.ts +3 -0
- package/dist/hooks/use-ps-menu-popup-reveal.js +28 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +8 -0
- package/dist/interfaces/ps-capture-options.d.ts +2 -0
- package/dist/interfaces/ps-document-scanner-options.d.ts +2 -0
- package/dist/interfaces/ps-image-manipulator-native.d.ts +19 -0
- package/dist/interfaces/ps-image-manipulator-native.js +1 -0
- package/dist/interfaces/ps-infinite-data-options.d.ts +10 -0
- package/dist/interfaces/ps-infinite-data-options.js +1 -0
- package/dist/interfaces/ps-infinite-data-result.d.ts +12 -0
- package/dist/interfaces/ps-infinite-data-result.js +1 -0
- package/dist/interfaces/ps-infinite-data-state.d.ts +8 -0
- package/dist/interfaces/ps-infinite-data-state.js +1 -0
- package/dist/interfaces/ps-infinite-data-update.d.ts +1 -0
- package/dist/interfaces/ps-infinite-data-update.js +1 -0
- package/dist/interfaces/ps-infinite-data-write.d.ts +6 -0
- package/dist/interfaces/ps-infinite-data-write.js +1 -0
- package/dist/interfaces/ps-infinite-page.d.ts +4 -0
- package/dist/interfaces/ps-infinite-page.js +1 -0
- package/dist/interfaces/ps-menu-popup-container-style.d.ts +8 -0
- package/dist/interfaces/ps-menu-popup-container-style.js +1 -0
- package/dist/interfaces/ps-menu-popup-divider-style.d.ts +4 -0
- package/dist/interfaces/ps-menu-popup-divider-style.js +1 -0
- package/dist/interfaces/ps-menu-popup-layout.d.ts +1 -0
- package/dist/interfaces/ps-menu-popup-row-style.d.ts +6 -0
- package/dist/interfaces/ps-menu-popup-row-style.js +1 -0
- package/dist/interfaces/ps-menu-popup-size.d.ts +4 -0
- package/dist/interfaces/ps-menu-popup-size.js +1 -0
- package/dist/interfaces/ps-page-size.d.ts +4 -0
- package/dist/interfaces/ps-page-size.js +1 -0
- package/dist/interfaces/ps-pdf-page.d.ts +5 -0
- package/dist/interfaces/ps-pdf-page.js +1 -0
- package/dist/interfaces/ps-scan-format-labels.d.ts +7 -0
- package/dist/interfaces/ps-scan-format-labels.js +1 -0
- package/dist/services/ps-capture-picker-service.js +1 -0
- package/dist/services/ps-document-scanner-service.d.ts +2 -0
- package/dist/services/ps-document-scanner-service.js +21 -4
- package/dist/services/ps-infinite-data-service.d.ts +8 -0
- package/dist/services/ps-infinite-data-service.js +22 -0
- package/dist/services/ps-infinite-list-service.d.ts +10 -0
- package/dist/services/ps-infinite-list-service.js +36 -0
- package/dist/services/ps-jpeg-size-service.d.ts +4 -0
- package/dist/services/ps-jpeg-size-service.js +32 -0
- package/dist/services/ps-menu-popup-service.js +1 -0
- package/dist/services/ps-menu-popup-style-service.d.ts +13 -0
- package/dist/services/ps-menu-popup-style-service.js +55 -0
- package/dist/services/ps-scan-format-prompt-service.d.ts +6 -0
- package/dist/services/ps-scan-format-prompt-service.js +21 -0
- package/dist/services/ps-scan-image-converter-service.d.ts +5 -0
- package/dist/services/ps-scan-image-converter-service.js +22 -0
- package/dist/services/ps-scan-output-service.d.ts +10 -0
- package/dist/services/ps-scan-output-service.js +55 -0
- package/dist/services/ps-scan-pdf-service.d.ts +4 -0
- package/dist/services/ps-scan-pdf-service.js +6 -0
- package/jest-setup.js +1 -1
- package/package.json +6 -2
|
@@ -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,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 @@
|
|
|
1
|
+
export {};
|
|
@@ -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(
|
|
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:
|
|
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
|
|
6
|
-
return _jsx(View, { style: {
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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,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,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,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
|
+
};
|