@pallasoft/ps-mobile-components 1.0.0 → 1.2.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-count-badge/index.d.ts +2 -0
- package/dist/components/ps-count-badge/index.js +2 -0
- package/dist/components/ps-count-badge/ps-count-badge-props.d.ts +5 -0
- package/dist/components/ps-count-badge/ps-count-badge-props.js +1 -0
- package/dist/components/ps-count-badge/ps-count-badge.d.ts +2 -0
- package/dist/components/ps-count-badge/ps-count-badge.js +25 -0
- package/dist/components/ps-list/index.d.ts +2 -0
- package/dist/components/ps-list/index.js +2 -0
- package/dist/components/ps-list/ps-list-link-row-props.d.ts +11 -0
- package/dist/components/ps-list/ps-list-link-row-props.js +1 -0
- package/dist/components/ps-list/ps-list-link-row-skeleton.d.ts +1 -0
- package/dist/components/ps-list/ps-list-link-row-skeleton.js +14 -0
- package/dist/components/ps-list/ps-list-link-row.d.ts +2 -0
- package/dist/components/ps-list/ps-list-link-row.js +40 -0
- package/dist/constants/ps-count-badge-config.d.ts +6 -0
- package/dist/constants/ps-count-badge-config.js +6 -0
- package/dist/constants/ps-layout-config.d.ts +4 -0
- package/dist/constants/ps-layout-config.js +4 -0
- package/dist/hooks/use-ps-remote-data.d.ts +1 -1
- package/dist/hooks/use-ps-remote-data.js +58 -28
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/interfaces/ps-remote-data-options.d.ts +1 -0
- package/dist/interfaces/ps-remote-data-update.d.ts +1 -0
- package/dist/interfaces/ps-remote-data-update.js +1 -0
- package/dist/interfaces/ps-remote-data-write.d.ts +4 -0
- package/dist/interfaces/ps-remote-data-write.js +1 -0
- package/dist/services/ps-count-badge-service.d.ts +3 -0
- package/dist/services/ps-count-badge-service.js +6 -0
- package/dist/services/ps-remote-data-service.d.ts +6 -0
- package/dist/services/ps-remote-data-service.js +13 -0
- package/package.json +2 -2
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { View } from 'react-native';
|
|
3
|
+
import { PsCountBadgeConfig } from '../../constants/ps-count-badge-config';
|
|
4
|
+
import { usePsMobileTheme } from '../../hooks/use-ps-mobile-theme';
|
|
5
|
+
import { PsCountBadgeService } from '../../services/ps-count-badge-service';
|
|
6
|
+
import { PsText } from '../ps-text/ps-text';
|
|
7
|
+
export const PsCountBadge = ({ count, isAlert = false, label }) => {
|
|
8
|
+
const theme = usePsMobileTheme();
|
|
9
|
+
if (count <= 0)
|
|
10
|
+
return null;
|
|
11
|
+
return (_jsx(View, { accessibilityLabel: label, style: {
|
|
12
|
+
alignItems: 'center',
|
|
13
|
+
alignSelf: 'center',
|
|
14
|
+
backgroundColor: isAlert ? theme.danger : theme.primary,
|
|
15
|
+
borderRadius: PsCountBadgeConfig.height / 2,
|
|
16
|
+
height: PsCountBadgeConfig.height,
|
|
17
|
+
justifyContent: 'center',
|
|
18
|
+
minWidth: PsCountBadgeConfig.height,
|
|
19
|
+
paddingHorizontal: PsCountBadgeConfig.paddingHorizontal,
|
|
20
|
+
}, testID: 'ps-count-badge', children: _jsx(PsText, { style: {
|
|
21
|
+
color: isAlert ? theme.white : theme.primaryContrast,
|
|
22
|
+
fontSize: PsCountBadgeConfig.fontSize,
|
|
23
|
+
lineHeight: PsCountBadgeConfig.height,
|
|
24
|
+
}, weight: 'bold', children: PsCountBadgeService.text(count) }) }));
|
|
25
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { PsMobileIconKey } from '../../constants/ps-mobile-icon-key';
|
|
2
|
+
export interface PsListLinkRowProps {
|
|
3
|
+
count?: number;
|
|
4
|
+
countLabel?: string;
|
|
5
|
+
iconKey: PsMobileIconKey;
|
|
6
|
+
isAlert?: boolean;
|
|
7
|
+
isLoading?: boolean;
|
|
8
|
+
onPress: () => void;
|
|
9
|
+
subtitle?: string;
|
|
10
|
+
title: string;
|
|
11
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const PsListLinkRowSkeleton: () => import("react").JSX.Element;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { View } from 'react-native';
|
|
3
|
+
import { PsLayoutConfig } from '../../constants/ps-layout-config';
|
|
4
|
+
import { PsListConfig } from '../../constants/ps-list-config';
|
|
5
|
+
import { PsSkeletonShape } from '../../constants/ps-skeleton-shape';
|
|
6
|
+
import { PsSkeleton } from '../ps-skeleton/ps-skeleton';
|
|
7
|
+
export const PsListLinkRowSkeleton = () => (_jsxs(View, { style: {
|
|
8
|
+
alignItems: 'center',
|
|
9
|
+
flexDirection: 'row',
|
|
10
|
+
gap: PsLayoutConfig.headerSlotGap,
|
|
11
|
+
minHeight: PsLayoutConfig.linkRowMinHeight,
|
|
12
|
+
paddingHorizontal: PsLayoutConfig.gutter,
|
|
13
|
+
paddingVertical: PsLayoutConfig.rowPaddingVertical,
|
|
14
|
+
}, testID: 'ps-list-link-row-skeleton', children: [_jsx(PsSkeleton, { height: PsLayoutConfig.linkRowIconBox, radius: PsLayoutConfig.linkRowIconBoxRadius, width: PsLayoutConfig.linkRowIconBox }), _jsxs(View, { style: { flex: 1, gap: PsLayoutConfig.rowLineGap * 3 }, children: [_jsx(PsSkeleton, { shape: PsSkeletonShape.Line, width: PsListConfig.skeletonTitleWidth }), _jsx(PsSkeleton, { shape: PsSkeletonShape.Line, width: PsListConfig.skeletonSubtitleWidth })] })] }));
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { PsIconKey } from '@pallasoft/ps-icons';
|
|
3
|
+
import { Pressable, View } from 'react-native';
|
|
4
|
+
import { PsLayoutConfig } from '../../constants/ps-layout-config';
|
|
5
|
+
import { PsMobileControlConfig } from '../../constants/ps-mobile-control-config';
|
|
6
|
+
import { usePsHaptics } from '../../hooks/use-ps-haptics';
|
|
7
|
+
import { usePsMobileTheme } from '../../hooks/use-ps-mobile-theme';
|
|
8
|
+
import { PsMobileColorService } from '../../services/ps-mobile-color-service';
|
|
9
|
+
import { PsCountBadge } from '../ps-count-badge/ps-count-badge';
|
|
10
|
+
import { PsIcon } from '../ps-icon/ps-icon';
|
|
11
|
+
import { PsText } from '../ps-text/ps-text';
|
|
12
|
+
import { PsListLinkRowSkeleton } from './ps-list-link-row-skeleton';
|
|
13
|
+
export const PsListLinkRow = ({ count, countLabel, iconKey, isAlert = false, isLoading = false, onPress, subtitle, title, }) => {
|
|
14
|
+
const theme = usePsMobileTheme();
|
|
15
|
+
const haptic = usePsHaptics();
|
|
16
|
+
if (isLoading)
|
|
17
|
+
return _jsx(PsListLinkRowSkeleton, {});
|
|
18
|
+
const hasCount = count !== undefined && count > 0;
|
|
19
|
+
return (_jsxs(Pressable, { accessibilityHint: countLabel, accessibilityRole: 'button', onPress: () => {
|
|
20
|
+
haptic('light');
|
|
21
|
+
onPress();
|
|
22
|
+
}, style: ({ pressed }) => ({
|
|
23
|
+
alignItems: 'center',
|
|
24
|
+
backgroundColor: pressed
|
|
25
|
+
? PsMobileColorService.withAlpha(theme.ink, PsMobileControlConfig.pressedAlpha)
|
|
26
|
+
: theme.surface,
|
|
27
|
+
flexDirection: 'row',
|
|
28
|
+
gap: PsLayoutConfig.headerSlotGap,
|
|
29
|
+
minHeight: PsLayoutConfig.linkRowMinHeight,
|
|
30
|
+
paddingHorizontal: PsLayoutConfig.gutter,
|
|
31
|
+
paddingVertical: PsLayoutConfig.rowPaddingVertical,
|
|
32
|
+
}), testID: 'ps-list-link-row', children: [_jsx(View, { style: {
|
|
33
|
+
alignItems: 'center',
|
|
34
|
+
backgroundColor: theme.primarySoft,
|
|
35
|
+
borderRadius: PsLayoutConfig.linkRowIconBoxRadius,
|
|
36
|
+
height: PsLayoutConfig.linkRowIconBox,
|
|
37
|
+
justifyContent: 'center',
|
|
38
|
+
width: PsLayoutConfig.linkRowIconBox,
|
|
39
|
+
}, children: _jsx(PsIcon, { color: theme.primary, iconKey: iconKey, size: PsLayoutConfig.linkRowIconSize }) }), _jsxs(View, { style: { flex: 1, gap: PsLayoutConfig.rowLineGap }, children: [_jsx(PsText, { numberOfLines: 1, weight: 'semibold', children: title }), subtitle === undefined ? null : (_jsx(PsText, { numberOfLines: 1, tone: 'muted', variant: 'small', children: subtitle }))] }), hasCount ? _jsx(PsCountBadge, { count: count, isAlert: isAlert, label: countLabel }) : null, _jsx(PsIcon, { color: theme.muted, iconKey: PsIconKey.ChevronRight, size: PsMobileControlConfig.iconSizeInline })] }));
|
|
40
|
+
};
|
|
@@ -6,6 +6,10 @@ export declare const PsLayoutConfig: {
|
|
|
6
6
|
readonly headerGap: 4;
|
|
7
7
|
readonly headerSlotGap: 12;
|
|
8
8
|
readonly hitSlop: 8;
|
|
9
|
+
readonly linkRowIconBox: 44;
|
|
10
|
+
readonly linkRowIconBoxRadius: 12;
|
|
11
|
+
readonly linkRowIconSize: 22;
|
|
12
|
+
readonly linkRowMinHeight: 72;
|
|
9
13
|
readonly listGap: 24;
|
|
10
14
|
readonly listSectionGap: 8;
|
|
11
15
|
readonly longPressDelayMs: 350;
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { PsRemoteDataOptions } from '../interfaces/ps-remote-data-options';
|
|
2
2
|
import { PsRemoteDataResult } from '../interfaces/ps-remote-data-result';
|
|
3
|
-
export declare const usePsRemoteData: <T>({ key, load, refreshKey, }: PsRemoteDataOptions<T>) => PsRemoteDataResult<T>;
|
|
3
|
+
export declare const usePsRemoteData: <T>({ key, load, onWriteFailed, refreshKey, }: PsRemoteDataOptions<T>) => PsRemoteDataResult<T>;
|
|
@@ -1,18 +1,27 @@
|
|
|
1
|
-
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
-
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import { usePsChangeQueue, usePsOptimisticRows } from '@pallasoft/ps-utilities/react';
|
|
3
|
+
import { PsRemoteDataService } from '../services/ps-remote-data-service';
|
|
4
|
+
export const usePsRemoteData = ({ key, load, onWriteFailed, refreshKey, }) => {
|
|
3
5
|
const [state, setState] = useState({
|
|
4
6
|
data: null,
|
|
5
7
|
error: false,
|
|
6
8
|
isLoading: true,
|
|
7
9
|
});
|
|
10
|
+
const [isSaving, setIsSaving] = useState(false);
|
|
11
|
+
const isSavingRef = useRef(false);
|
|
12
|
+
const submittedRef = useRef(0);
|
|
8
13
|
const dataRef = useRef(null);
|
|
9
14
|
const loadRef = useRef(load);
|
|
15
|
+
const onWriteFailedRef = useRef(onWriteFailed);
|
|
10
16
|
const versionRef = useRef(0);
|
|
11
|
-
const queueRef = useRef(Promise.resolve(true));
|
|
12
|
-
const pendingRef = useRef(0);
|
|
13
17
|
useEffect(() => {
|
|
14
18
|
loadRef.current = load;
|
|
19
|
+
onWriteFailedRef.current = onWriteFailed;
|
|
15
20
|
});
|
|
21
|
+
const setSaving = useCallback((value) => {
|
|
22
|
+
isSavingRef.current = value;
|
|
23
|
+
setIsSaving(value);
|
|
24
|
+
}, []);
|
|
16
25
|
const fetch = useCallback(async () => {
|
|
17
26
|
versionRef.current += 1;
|
|
18
27
|
const version = versionRef.current;
|
|
@@ -31,42 +40,63 @@ export const usePsRemoteData = ({ key, load, refreshKey, }) => {
|
|
|
31
40
|
}, []);
|
|
32
41
|
useEffect(() => {
|
|
33
42
|
dataRef.current = null;
|
|
34
|
-
|
|
43
|
+
setSaving(false);
|
|
35
44
|
setState({ data: null, error: false, isLoading: true });
|
|
36
45
|
void fetch();
|
|
37
46
|
return () => {
|
|
38
47
|
versionRef.current += 1;
|
|
39
48
|
};
|
|
40
|
-
}, [key, fetch]);
|
|
49
|
+
}, [key, fetch, setSaving]);
|
|
41
50
|
const refreshRef = useRef(refreshKey);
|
|
42
51
|
useEffect(() => {
|
|
43
52
|
if (refreshRef.current === refreshKey)
|
|
44
53
|
return;
|
|
45
54
|
refreshRef.current = refreshKey;
|
|
46
|
-
if (
|
|
55
|
+
if (!isSavingRef.current)
|
|
47
56
|
void fetch();
|
|
48
57
|
}, [fetch, refreshKey]);
|
|
58
|
+
const apply = useCallback((rows, updates) => PsRemoteDataService.apply(rows, updates), []);
|
|
59
|
+
const rows = useMemo(() => PsRemoteDataService.rowsOf(state.data), [state.data]);
|
|
60
|
+
const optimistic = usePsOptimisticRows({ apply, isSaving, rows });
|
|
61
|
+
const rememberRef = useRef(optimistic.remember);
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
rememberRef.current = optimistic.remember;
|
|
64
|
+
});
|
|
65
|
+
const send = useCallback(async (writes) => {
|
|
66
|
+
for (const item of writes) {
|
|
67
|
+
await item.request();
|
|
68
|
+
item.resolve(true);
|
|
69
|
+
}
|
|
70
|
+
}, []);
|
|
71
|
+
const onFailed = useCallback((failed, dropped) => {
|
|
72
|
+
for (const item of [...failed, ...dropped]) {
|
|
73
|
+
optimistic.forgetLast();
|
|
74
|
+
item.resolve(false);
|
|
75
|
+
}
|
|
76
|
+
if (onWriteFailedRef.current !== undefined)
|
|
77
|
+
onWriteFailedRef.current();
|
|
78
|
+
}, [optimistic.forgetLast]);
|
|
79
|
+
const onDrained = useCallback(() => {
|
|
80
|
+
const through = submittedRef.current;
|
|
81
|
+
void fetch().then(() => {
|
|
82
|
+
if (submittedRef.current === through)
|
|
83
|
+
setSaving(false);
|
|
84
|
+
});
|
|
85
|
+
}, [fetch, setSaving]);
|
|
86
|
+
const queue = usePsChangeQueue({ onDrained, onFailed, send });
|
|
87
|
+
const { enqueue } = queue;
|
|
49
88
|
const write = useCallback((update, request) => {
|
|
50
|
-
|
|
51
|
-
if (previous === null)
|
|
89
|
+
if (dataRef.current === null)
|
|
52
90
|
return Promise.resolve(false);
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
setState({ data: previous, error: false, isLoading: false });
|
|
65
|
-
}
|
|
66
|
-
if (pendingRef.current === 0)
|
|
67
|
-
void fetch();
|
|
68
|
-
return isSaved;
|
|
69
|
-
});
|
|
70
|
-
}, [fetch]);
|
|
71
|
-
return { ...state, reload: fetch, write };
|
|
91
|
+
submittedRef.current += 1;
|
|
92
|
+
setSaving(true);
|
|
93
|
+
rememberRef.current(update);
|
|
94
|
+
return new Promise((resolve) => enqueue({ request, resolve }));
|
|
95
|
+
}, [enqueue, setSaving]);
|
|
96
|
+
return {
|
|
97
|
+
...state,
|
|
98
|
+
data: PsRemoteDataService.shown(optimistic.rows),
|
|
99
|
+
reload: fetch,
|
|
100
|
+
write,
|
|
101
|
+
};
|
|
72
102
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -25,6 +25,7 @@ export * from './components/ps-mobile-theme-provider';
|
|
|
25
25
|
export * from './components/ps-page-backdrop';
|
|
26
26
|
export * from './components/ps-pallasoft-sign-in-button';
|
|
27
27
|
export * from './components/ps-color-swatches';
|
|
28
|
+
export * from './components/ps-count-badge';
|
|
28
29
|
export * from './components/ps-segmented-control';
|
|
29
30
|
export * from './components/ps-tabs';
|
|
30
31
|
export * from './components/ps-select';
|
package/dist/index.js
CHANGED
|
@@ -25,6 +25,7 @@ export * from './components/ps-mobile-theme-provider';
|
|
|
25
25
|
export * from './components/ps-page-backdrop';
|
|
26
26
|
export * from './components/ps-pallasoft-sign-in-button';
|
|
27
27
|
export * from './components/ps-color-swatches';
|
|
28
|
+
export * from './components/ps-count-badge';
|
|
28
29
|
export * from './components/ps-segmented-control';
|
|
29
30
|
export * from './components/ps-tabs';
|
|
30
31
|
export * from './components/ps-select';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type PsRemoteDataUpdate<T> = (data: T) => T;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { PsRemoteDataUpdate } from '../interfaces/ps-remote-data-update';
|
|
2
|
+
export declare class PsRemoteDataService {
|
|
3
|
+
static rowsOf<T>(data: T | null): T[];
|
|
4
|
+
static apply<T>(rows: T[], updates: PsRemoteDataUpdate<T>[]): T[];
|
|
5
|
+
static shown<T>(rows: T[]): T | null;
|
|
6
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export class PsRemoteDataService {
|
|
2
|
+
static rowsOf(data) {
|
|
3
|
+
return data === null ? [] : [data];
|
|
4
|
+
}
|
|
5
|
+
static apply(rows, updates) {
|
|
6
|
+
if (rows.length === 0)
|
|
7
|
+
return rows;
|
|
8
|
+
return [updates.reduce((current, update) => update(current), rows[0])];
|
|
9
|
+
}
|
|
10
|
+
static shown(rows) {
|
|
11
|
+
return rows.length === 0 ? null : rows[0];
|
|
12
|
+
}
|
|
13
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pallasoft/ps-mobile-components",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.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",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"@pallasoft/ps-design-system": "^1.17.0",
|
|
38
38
|
"@pallasoft/ps-icons": "^1.11.0",
|
|
39
|
-
"@pallasoft/ps-utilities": "^2.
|
|
39
|
+
"@pallasoft/ps-utilities": "^2.27.0"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
42
|
"@expo/ui": "~58.0.0",
|