@pallasoft/ps-mobile-components 1.13.1 → 1.15.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 (46) hide show
  1. package/dist/components/ps-action-menu/ps-action-menu.android.js +2 -2
  2. package/dist/components/ps-action-menu/ps-menu-tile-drawing.js +7 -2
  3. package/dist/components/ps-link-preview-card/index.d.ts +2 -0
  4. package/dist/components/ps-link-preview-card/index.js +2 -0
  5. package/dist/components/ps-link-preview-card/ps-link-preview-card-props.d.ts +21 -0
  6. package/dist/components/ps-link-preview-card/ps-link-preview-card-props.js +1 -0
  7. package/dist/components/ps-link-preview-card/ps-link-preview-card-skeleton.d.ts +2 -0
  8. package/dist/components/ps-link-preview-card/ps-link-preview-card-skeleton.js +28 -0
  9. package/dist/components/ps-link-preview-card/ps-link-preview-card.d.ts +2 -0
  10. package/dist/components/ps-link-preview-card/ps-link-preview-card.js +66 -0
  11. package/dist/components/ps-list/ps-list-person-row.js +1 -3
  12. package/dist/components/ps-list/ps-list-row-body.js +1 -3
  13. package/dist/components/ps-list/ps-list-row-skeleton.js +1 -3
  14. package/dist/components/ps-menu-popup/ps-menu-popup-row.d.ts +4 -1
  15. package/dist/components/ps-menu-popup/ps-menu-popup-row.js +3 -2
  16. package/dist/components/ps-menu-popup/ps-menu-popup-tile.d.ts +6 -0
  17. package/dist/components/ps-menu-popup/ps-menu-popup-tile.js +27 -0
  18. package/dist/components/ps-native-menu/ps-native-menu-row.js +1 -1
  19. package/dist/components/ps-sheet-header/ps-sheet-header.js +2 -2
  20. package/dist/constants/ps-chart-config.d.ts +1 -0
  21. package/dist/constants/ps-chart-config.js +1 -0
  22. package/dist/constants/ps-layout-config.d.ts +2 -2
  23. package/dist/constants/ps-layout-config.js +2 -2
  24. package/dist/constants/ps-link-preview-config.d.ts +19 -0
  25. package/dist/constants/ps-link-preview-config.js +19 -0
  26. package/dist/constants/ps-link-preview-size.d.ts +5 -0
  27. package/dist/constants/ps-link-preview-size.js +4 -0
  28. package/dist/constants/ps-menu-popup-config.d.ts +4 -0
  29. package/dist/constants/ps-menu-popup-config.js +4 -0
  30. package/dist/constants/ps-sheet-config.d.ts +1 -0
  31. package/dist/constants/ps-sheet-config.js +1 -0
  32. package/dist/hooks/use-ps-chart-reveal.js +6 -2
  33. package/dist/index.d.ts +3 -0
  34. package/dist/index.js +3 -0
  35. package/dist/interfaces/ps-link-preview-meta.d.ts +5 -0
  36. package/dist/interfaces/ps-link-preview-meta.js +1 -0
  37. package/dist/interfaces/ps-menu-item.d.ts +1 -0
  38. package/dist/interfaces/ps-menu-tile-request.d.ts +1 -0
  39. package/dist/services/ps-chart-reveal-service.d.ts +4 -0
  40. package/dist/services/ps-chart-reveal-service.js +9 -0
  41. package/dist/services/ps-link-preview-service.d.ts +12 -0
  42. package/dist/services/ps-link-preview-service.js +38 -0
  43. package/dist/services/ps-menu-tile-service.js +10 -4
  44. package/dist/services/ps-sheet-service.d.ts +1 -0
  45. package/dist/services/ps-sheet-service.js +3 -0
  46. package/package.json +1 -1
@@ -19,9 +19,9 @@ export const PsActionMenu = ({ children, header, isDisabled = false, items, labe
19
19
  if (!isDisabled)
20
20
  menu.open();
21
21
  },
22
- }, children: children }), menu.anchor === undefined ? null : (_jsxs(PsMenuPopup, { anchor: menu.anchor, onClose: menu.close, children: [header === undefined ? null : (_jsx(PsMenuPopupHeader, { subtitle: header.subtitle, title: header.title })), groups.map((group, index) => (_jsxs(Fragment, { children: [index > 0 || header !== undefined ? _jsx(PsMenuPopupDivider, {}) : null, PsActionMenuService.flatten([group]).map((item) => (_jsx(PsMenuPopupRow, { iconKey: item.iconKey, isChecked: item.isChecked, isDanger: item.isDanger, isDisabled: item.isDisabled, isWarning: item.isWarning, label: item.label, onPress: () => {
22
+ }, children: children }), menu.anchor === undefined ? null : (_jsxs(PsMenuPopup, { anchor: menu.anchor, onClose: menu.close, children: [header === undefined ? null : (_jsx(PsMenuPopupHeader, { subtitle: header.subtitle, title: header.title })), groups.map((group, index) => (_jsxs(Fragment, { children: [index > 0 || header !== undefined ? _jsx(PsMenuPopupDivider, {}) : null, PsActionMenuService.flatten([group]).map((item) => (_jsx(PsMenuPopupRow, { iconKey: item.iconKey, imageUri: item.imageUri, isChecked: item.isChecked, isDanger: item.isDanger, isDisabled: item.isDisabled, isWarning: item.isWarning, label: item.label, monogram: item.monogram, onPress: () => {
23
23
  menu.close();
24
24
  haptic('selection');
25
25
  item.onSelect();
26
- }, subtitle: item.subtitle }, item.id)))] }, group.id)))] }))] }));
26
+ }, subtitle: item.subtitle, tileColor: item.tileColor }, item.id)))] }, group.id)))] }))] }));
27
27
  };
@@ -1,5 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useId, useRef, useState } from 'react';
3
+ import { PallasoftColorContrastService } from '@pallasoft/ps-design-system';
3
4
  import Svg, { ClipPath, Defs, G, Rect, Image as SvgImage, Text as SvgText } from 'react-native-svg';
4
5
  import { PsMenuTileConfig } from '../../constants/ps-menu-tile-config';
5
6
  import { usePsMobileTheme } from '../../hooks/use-ps-mobile-theme';
@@ -14,6 +15,10 @@ export const PsMenuTileDrawing = ({ onTile, request }) => {
14
15
  const isMounted = useRef(true);
15
16
  const [isLaidOut, setIsLaidOut] = useState(false);
16
17
  const [isImageLoaded, setIsImageLoaded] = useState(false);
18
+ const fill = request.tileColor === undefined ? theme.primary : request.tileColor;
19
+ const textColor = request.tileColor === undefined
20
+ ? theme.primaryContrast
21
+ : PallasoftColorContrastService.textOn(request.tileColor);
17
22
  const isReady = isLaidOut && (request.imageUri === undefined || isImageLoaded);
18
23
  useEffect(() => {
19
24
  isMounted.current = true;
@@ -29,6 +34,6 @@ export const PsMenuTileDrawing = ({ onTile, request }) => {
29
34
  if (isMounted.current)
30
35
  onTile(request.key, `${PsMenuTileConfig.dataUriPrefix}${base64}`);
31
36
  });
32
- }, [isReady, onTile, request.key, theme.primary, theme.primaryContrast]);
33
- return (_jsxs(Svg, { height: size, onLayout: () => setIsLaidOut(true), ref: ref, width: size, children: [_jsx(Defs, { children: _jsx(ClipPath, { id: clipId, children: _jsx(Rect, { height: size, rx: isCircle ? size / 2 : PsMenuTileConfig.roundedRadius, width: size }) }) }), _jsxs(G, { clipPath: `url(#${clipId})`, children: [_jsx(Rect, { fill: request.imageUri === undefined ? theme.primary : theme.white, height: size, width: size }), request.imageUri === undefined ? (_jsx(SvgText, { fill: theme.primaryContrast, fontFamily: theme.fonts.semibold, fontSize: fontSize, textAnchor: 'middle', x: size / 2, y: size / 2 + fontSize * PsMenuTileConfig.textBaseline, children: request.text })) : (_jsx(SvgImage, { height: size - inset * 2, href: { uri: request.imageUri }, onLoad: () => setIsImageLoaded(true), preserveAspectRatio: isCircle ? 'xMidYMid slice' : 'xMidYMid meet', width: size - inset * 2, x: inset, y: inset }))] })] }));
37
+ }, [isReady, onTile, request.key, fill, textColor]);
38
+ return (_jsxs(Svg, { height: size, onLayout: () => setIsLaidOut(true), ref: ref, width: size, children: [_jsx(Defs, { children: _jsx(ClipPath, { id: clipId, children: _jsx(Rect, { height: size, rx: isCircle ? size / 2 : PsMenuTileConfig.roundedRadius, width: size }) }) }), _jsxs(G, { clipPath: `url(#${clipId})`, children: [_jsx(Rect, { fill: request.imageUri === undefined ? fill : theme.white, height: size, width: size }), request.imageUri === undefined ? (_jsx(SvgText, { fill: textColor, fontFamily: theme.fonts.semibold, fontSize: fontSize, textAnchor: 'middle', x: size / 2, y: size / 2 + fontSize * PsMenuTileConfig.textBaseline, children: request.text })) : (_jsx(SvgImage, { height: size - inset * 2, href: { uri: request.imageUri }, onLoad: () => setIsImageLoaded(true), preserveAspectRatio: isCircle ? 'xMidYMid slice' : 'xMidYMid meet', width: size - inset * 2, x: inset, y: inset }))] })] }));
34
39
  };
@@ -0,0 +1,2 @@
1
+ export * from './ps-link-preview-card';
2
+ export * from './ps-link-preview-card-props';
@@ -0,0 +1,2 @@
1
+ export * from './ps-link-preview-card';
2
+ export * from './ps-link-preview-card-props';
@@ -0,0 +1,21 @@
1
+ import type { PsLinkPreviewSize } from '../../constants/ps-link-preview-size';
2
+ import type { PsDocumentSource } from '../../interfaces/ps-document-source';
3
+ export interface PsLinkPreviewCardProps {
4
+ description?: string | null;
5
+ faviconSource?: PsDocumentSource | null;
6
+ href: string;
7
+ imageSource?: PsDocumentSource | null;
8
+ isFramed?: boolean;
9
+ isLoading?: boolean;
10
+ loadingLabel?: string;
11
+ onPress: () => void;
12
+ opensInBrowserLabel?: string;
13
+ siteName?: string | null;
14
+ size?: PsLinkPreviewSize;
15
+ title?: string | null;
16
+ }
17
+ export interface PsLinkPreviewCardSkeletonProps {
18
+ isCompact: boolean;
19
+ isFramed: boolean;
20
+ label?: string;
21
+ }
@@ -0,0 +1,2 @@
1
+ import type { PsLinkPreviewCardSkeletonProps } from './ps-link-preview-card-props';
2
+ export declare const PsLinkPreviewCardSkeleton: ({ isCompact, isFramed, label, }: PsLinkPreviewCardSkeletonProps) => import("react").JSX.Element;
@@ -0,0 +1,28 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { View } from 'react-native';
4
+ import { PsLinkPreviewConfig } from '../../constants/ps-link-preview-config';
5
+ import { PsMobileControlConfig } from '../../constants/ps-mobile-control-config';
6
+ import { PsSkeletonShape } from '../../constants/ps-skeleton-shape';
7
+ import { usePsMobileTheme } from '../../hooks/use-ps-mobile-theme';
8
+ import { PsLinkPreviewService } from '../../services/ps-link-preview-service';
9
+ import { PsSkeleton } from '../ps-skeleton/ps-skeleton';
10
+ export const PsLinkPreviewCardSkeleton = ({ isCompact, isFramed, label, }) => {
11
+ const theme = usePsMobileTheme();
12
+ const [width, setWidth] = useState(0);
13
+ const bodyPadding = isCompact
14
+ ? PsLinkPreviewConfig.compactBodyPadding
15
+ : PsLinkPreviewConfig.fullBodyPadding;
16
+ return (_jsxs(View, { accessibilityLabel: label, accessible: label !== undefined, onLayout: (event) => setWidth(event.nativeEvent.layout.width), style: {
17
+ backgroundColor: theme.surface,
18
+ borderColor: theme.line,
19
+ borderRadius: PsMobileControlConfig.cardRadius,
20
+ borderWidth: isFramed ? 1 : 0,
21
+ flexDirection: isCompact ? 'row' : 'column',
22
+ overflow: 'hidden',
23
+ }, testID: 'ps-link-preview-card-skeleton', children: [isCompact ? (_jsx(PsSkeleton, { height: PsLinkPreviewConfig.compactImageMinHeight, radius: 0, shape: PsSkeletonShape.Block, width: PsLinkPreviewConfig.compactImageWidth })) : (_jsx(PsSkeleton, { height: PsLinkPreviewService.imageHeight(width), radius: 0, shape: PsSkeletonShape.Block })), _jsxs(View, { style: {
24
+ flex: isCompact ? 1 : undefined,
25
+ gap: PsLinkPreviewConfig.skeletonLineGap,
26
+ padding: bodyPadding,
27
+ }, children: [_jsx(PsSkeleton, { shape: PsSkeletonShape.Line, width: PsLinkPreviewConfig.skeletonMetaWidth }), _jsx(PsSkeleton, { shape: PsSkeletonShape.Line, width: PsLinkPreviewConfig.skeletonTitleWidth }), isCompact ? null : _jsx(PsSkeleton, { shape: PsSkeletonShape.Line })] })] }));
28
+ };
@@ -0,0 +1,2 @@
1
+ import type { PsLinkPreviewCardProps } from './ps-link-preview-card-props';
2
+ export declare const PsLinkPreviewCard: ({ description, faviconSource, href, imageSource, isFramed, isLoading, loadingLabel, onPress, opensInBrowserLabel, siteName, size, title, }: PsLinkPreviewCardProps) => import("react").JSX.Element | null;
@@ -0,0 +1,66 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { PsIconKey } from '@pallasoft/ps-icons';
4
+ import { Image, Pressable, View } from 'react-native';
5
+ import { PsLinkPreviewConfig } from '../../constants/ps-link-preview-config';
6
+ import { PsLinkPreviewSize } from '../../constants/ps-link-preview-size';
7
+ import { PsMobileControlConfig } from '../../constants/ps-mobile-control-config';
8
+ import { PsRippleKind } from '../../constants/ps-ripple-kind';
9
+ import { usePsHaptics } from '../../hooks/use-ps-haptics';
10
+ import { usePsMobileTheme } from '../../hooks/use-ps-mobile-theme';
11
+ import { PsLinkPreviewService } from '../../services/ps-link-preview-service';
12
+ import { PsRippleService } from '../../services/ps-ripple-service';
13
+ import { PsStateService } from '../../services/ps-state-service';
14
+ import { PsIcon } from '../ps-icon/ps-icon';
15
+ import { PsText } from '../ps-text/ps-text';
16
+ import { PsLinkPreviewCardSkeleton } from './ps-link-preview-card-skeleton';
17
+ export const PsLinkPreviewCard = ({ description, faviconSource, href, imageSource, isFramed = true, isLoading = false, loadingLabel, onPress, opensInBrowserLabel, siteName, size = PsLinkPreviewSize.Full, title, }) => {
18
+ const theme = usePsMobileTheme();
19
+ const haptic = usePsHaptics();
20
+ const [failedImageUri, setFailedImageUri] = useState(null);
21
+ const [failedFaviconUri, setFailedFaviconUri] = useState(null);
22
+ if (!PsLinkPreviewService.isWebAddress(href))
23
+ return null;
24
+ const isCompact = PsLinkPreviewService.isCompact(size);
25
+ if (isLoading)
26
+ return (_jsx(PsLinkPreviewCardSkeleton, { isCompact: isCompact, isFramed: isFramed, label: loadingLabel }));
27
+ const meta = PsLinkPreviewService.meta(href, title, siteName, description, size);
28
+ const hasImage = PsLinkPreviewService.hasSource(imageSource, failedImageUri);
29
+ const hasFavicon = PsLinkPreviewService.hasSource(faviconSource, failedFaviconUri);
30
+ return (_jsxs(Pressable, { accessibilityHint: opensInBrowserLabel, accessibilityRole: 'link', android_ripple: PsRippleService.resolve(theme, PsRippleKind.Surface), onPress: () => {
31
+ haptic('light');
32
+ onPress();
33
+ }, style: ({ pressed }) => ({
34
+ backgroundColor: pressed ? PsStateService.pressedFill(theme, theme.surface) : theme.surface,
35
+ borderColor: theme.line,
36
+ borderRadius: PsMobileControlConfig.cardRadius,
37
+ borderWidth: isFramed ? 1 : 0,
38
+ flexDirection: isCompact ? 'row' : 'column',
39
+ overflow: 'hidden',
40
+ }), testID: 'ps-link-preview-card', children: [hasImage && imageSource !== null && imageSource !== undefined ? (_jsx(Image, { onError: () => setFailedImageUri(imageSource.uri), resizeMode: 'cover', source: { headers: imageSource.headers, uri: imageSource.uri }, style: isCompact
41
+ ? {
42
+ alignSelf: 'stretch',
43
+ backgroundColor: theme.skeletonBase,
44
+ minHeight: PsLinkPreviewConfig.compactImageMinHeight,
45
+ width: PsLinkPreviewConfig.compactImageWidth,
46
+ }
47
+ : {
48
+ aspectRatio: PsLinkPreviewConfig.imageAspectRatio,
49
+ backgroundColor: theme.skeletonBase,
50
+ width: '100%',
51
+ }, testID: 'ps-link-preview-card-image' })) : null, _jsxs(View, { style: {
52
+ flex: isCompact ? 1 : undefined,
53
+ gap: PsLinkPreviewConfig.textGap,
54
+ padding: isCompact
55
+ ? PsLinkPreviewConfig.compactBodyPadding
56
+ : PsLinkPreviewConfig.fullBodyPadding,
57
+ }, children: [_jsxs(View, { style: {
58
+ alignItems: 'center',
59
+ flexDirection: 'row',
60
+ gap: PsLinkPreviewConfig.metaGap,
61
+ }, children: [hasFavicon && faviconSource !== null && faviconSource !== undefined ? (_jsx(Image, { onError: () => setFailedFaviconUri(faviconSource.uri), resizeMode: 'contain', source: { headers: faviconSource.headers, uri: faviconSource.uri }, style: {
62
+ borderRadius: PsLinkPreviewConfig.faviconRadius,
63
+ height: PsLinkPreviewConfig.faviconSize,
64
+ width: PsLinkPreviewConfig.faviconSize,
65
+ }, testID: 'ps-link-preview-card-favicon' })) : (_jsx(PsIcon, { color: theme.muted, iconKey: PsIconKey.Globe, size: PsLinkPreviewConfig.faviconSize })), _jsx(View, { style: { flex: 1 }, children: _jsx(PsText, { numberOfLines: 1, tone: 'muted', variant: 'caption', weight: 'medium', children: meta.siteLabel }) }), _jsx(PsIcon, { color: theme.muted, iconKey: PsIconKey.ExternalLink, size: PsLinkPreviewConfig.faviconSize })] }), meta.hasTitle ? (_jsx(PsText, { numberOfLines: PsLinkPreviewConfig.titleLines, weight: 'bold', children: title })) : (_jsx(PsText, { numberOfLines: 1, tone: 'muted', variant: 'caption', children: href })), meta.hasDescription ? (_jsx(PsText, { numberOfLines: PsLinkPreviewConfig.descriptionLines, tone: 'muted', variant: 'small', children: description })) : null] })] }));
66
+ };
@@ -12,9 +12,7 @@ export const PsListPersonRow = ({ badge, initials, subtitle, title }) => {
12
12
  backgroundColor: theme.surface,
13
13
  flexDirection: 'row',
14
14
  gap: PsLayoutConfig.headerSlotGap,
15
- minHeight: subtitle === undefined
16
- ? PsLayoutConfig.rowMinHeight
17
- : PsLayoutConfig.rowMinHeightWithSubtitle,
15
+ minHeight: PsLayoutConfig.rowMinHeight,
18
16
  paddingHorizontal: PsLayoutConfig.gutter,
19
17
  paddingVertical: PsLayoutConfig.rowPaddingVertical,
20
18
  }, testID: 'ps-list-person-row', children: [_jsx(PsAvatar, { initials: initials, size: 'small' }), _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 }))] }), badge === undefined ? null : (_jsx(PsBadge, { size: 'small', tone: badge.tone, children: badge.label }))] }));
@@ -21,9 +21,7 @@ export const PsListRowBody = ({ badge, hasChevron = false, hasMenu, isSelectable
21
21
  backgroundColor: PsListRowStyleService.background(isSelectable && isSelected, pressed, theme),
22
22
  flexDirection: 'row',
23
23
  gap: PsLayoutConfig.headerSlotGap,
24
- minHeight: hasSecondLine
25
- ? PsLayoutConfig.rowMinHeightWithSubtitle
26
- : PsLayoutConfig.rowMinHeight,
24
+ minHeight: PsLayoutConfig.rowMinHeight,
27
25
  paddingHorizontal: PsLayoutConfig.gutter,
28
26
  paddingVertical: PsLayoutConfig.rowPaddingVertical,
29
27
  }), testID: 'ps-list-row', children: [isSelectable ? _jsx(PsListRowSelectMark, { isSelected: isSelected }) : null, leadingIcon === undefined ? null : (_jsx(View, { style: {
@@ -16,9 +16,7 @@ export const PsListRowSkeleton = ({ badge, isSection = false, leadingIcon, skele
16
16
  alignItems: 'center',
17
17
  flexDirection: 'row',
18
18
  gap: PsLayoutConfig.headerSlotGap,
19
- minHeight: hasSubtitle
20
- ? PsLayoutConfig.rowMinHeightWithSubtitle
21
- : PsLayoutConfig.rowMinHeight,
19
+ minHeight: PsLayoutConfig.rowMinHeight,
22
20
  paddingHorizontal: PsLayoutConfig.gutter,
23
21
  paddingVertical: PsLayoutConfig.rowPaddingVertical,
24
22
  }, testID: 'ps-skeleton-row', children: [hasIcon ? (_jsx(PsSkeleton, { height: PsLayoutConfig.rowIconBox, radius: PsLayoutConfig.rowIconBoxRadius, width: PsLayoutConfig.rowIconBox })) : null, _jsxs(View, { style: { flex: 1, gap: PsLayoutConfig.rowLineGap * 3 }, children: [_jsxs(View, { style: {
@@ -1,12 +1,15 @@
1
1
  import type { PsMobileIconKey } from '../../constants/ps-mobile-icon-key';
2
2
  export interface PsMenuPopupRowProps {
3
3
  iconKey?: PsMobileIconKey;
4
+ imageUri?: string;
4
5
  isChecked?: boolean;
5
6
  isDanger?: boolean;
6
7
  isDisabled?: boolean;
7
8
  isWarning?: boolean;
8
9
  label: string;
10
+ monogram?: string;
9
11
  onPress: () => void;
10
12
  subtitle?: string;
13
+ tileColor?: string;
11
14
  }
12
- export declare const PsMenuPopupRow: ({ iconKey, isChecked, isDanger, isDisabled, isWarning, label, onPress, subtitle, }: PsMenuPopupRowProps) => import("react").JSX.Element;
15
+ export declare const PsMenuPopupRow: ({ iconKey, imageUri, isChecked, isDanger, isDisabled, isWarning, label, monogram, onPress, subtitle, tileColor, }: PsMenuPopupRowProps) => import("react").JSX.Element;
@@ -11,7 +11,8 @@ import { PsRippleService } from '../../services/ps-ripple-service';
11
11
  import { PsStateService } from '../../services/ps-state-service';
12
12
  import { PsIcon } from '../ps-icon/ps-icon';
13
13
  import { PsText } from '../ps-text/ps-text';
14
- export const PsMenuPopupRow = ({ iconKey, isChecked = false, isDanger = false, isDisabled = false, isWarning = false, label, onPress, subtitle, }) => {
14
+ import { PsMenuPopupTile } from './ps-menu-popup-tile';
15
+ export const PsMenuPopupRow = ({ iconKey, imageUri, isChecked = false, isDanger = false, isDisabled = false, isWarning = false, label, monogram, onPress, subtitle, tileColor, }) => {
15
16
  const theme = usePsMobileTheme();
16
17
  const scheme = usePsMaterialScheme();
17
18
  const isMaterial = PsMenuPopupStyleService.isMaterial();
@@ -30,5 +31,5 @@ export const PsMenuPopupRow = ({ iconKey, isChecked = false, isDanger = false, i
30
31
  overflow: 'hidden',
31
32
  paddingHorizontal: row.paddingHorizontal,
32
33
  };
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] }));
34
+ }, children: [imageUri !== undefined || monogram !== undefined ? (_jsx(PsMenuPopupTile, { imageUri: imageUri, monogram: monogram, tileColor: tileColor })) : 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] }));
34
35
  };
@@ -0,0 +1,6 @@
1
+ export interface PsMenuPopupTileProps {
2
+ imageUri?: string;
3
+ monogram?: string;
4
+ tileColor?: string;
5
+ }
6
+ export declare const PsMenuPopupTile: ({ imageUri, monogram, tileColor }: PsMenuPopupTileProps) => import("react").JSX.Element;
@@ -0,0 +1,27 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { PallasoftColorContrastService } from '@pallasoft/ps-design-system';
3
+ import { Image, View } from 'react-native';
4
+ import { PsMenuPopupConfig } from '../../constants/ps-menu-popup-config';
5
+ import { usePsMobileTheme } from '../../hooks/use-ps-mobile-theme';
6
+ import { PsMenuTileService } from '../../services/ps-menu-tile-service';
7
+ import { PsText } from '../ps-text/ps-text';
8
+ export const PsMenuPopupTile = ({ imageUri, monogram, tileColor }) => {
9
+ const theme = usePsMobileTheme();
10
+ const size = PsMenuPopupConfig.tileSize;
11
+ const fill = tileColor === undefined ? theme.primary : tileColor;
12
+ const textColor = tileColor === undefined
13
+ ? theme.primaryContrast
14
+ : PallasoftColorContrastService.textOn(tileColor);
15
+ return (_jsx(View, { style: {
16
+ alignItems: 'center',
17
+ backgroundColor: imageUri === undefined ? fill : theme.white,
18
+ borderRadius: PsMenuPopupConfig.tileRadius,
19
+ height: size,
20
+ justifyContent: 'center',
21
+ overflow: 'hidden',
22
+ width: size,
23
+ }, testID: 'ps-menu-popup-tile', children: imageUri === undefined ? (_jsx(PsText, { style: { color: textColor, fontSize: PsMenuPopupConfig.tileFontSize }, weight: 'semibold', children: PsMenuTileService.letter(monogram === undefined ? '' : monogram) })) : (_jsx(Image, { resizeMode: 'contain', source: { uri: imageUri }, style: {
24
+ height: size - PsMenuPopupConfig.tileLogoPadding * 2,
25
+ width: size - PsMenuPopupConfig.tileLogoPadding * 2,
26
+ } })) }));
27
+ };
@@ -16,7 +16,7 @@ export const PsNativeMenuRow = ({ item, onSelect }) => {
16
16
  backgroundColor: pressed ? PsStateService.pressedFill(theme, theme.surface) : theme.surface,
17
17
  flexDirection: 'row',
18
18
  gap: PsMobileControlConfig.rowSpacing,
19
- minHeight: PsLayoutConfig.rowMinHeight,
19
+ minHeight: PsLayoutConfig.menuRowMinHeight,
20
20
  paddingHorizontal: PsLayoutConfig.gutter,
21
21
  }), children: [_jsx(PsText, { style: { flex: 1 }, weight: item.isSelected ? 'semibold' : 'regular', children: item.label }), item.isSelected ? _jsx(PsIcon, { color: theme.primary, iconKey: PsIconKey.Check }) : null] }));
22
22
  };
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { PsIconKey } from '@pallasoft/ps-icons';
3
- import { StyleSheet, View } from 'react-native';
3
+ import { Platform, StyleSheet, View } from 'react-native';
4
4
  import { PsPageHeaderConfig } from '../../constants/ps-page-header-config';
5
5
  import { PsSheetConfig } from '../../constants/ps-sheet-config';
6
6
  import { usePsMobileTheme } from '../../hooks/use-ps-mobile-theme';
@@ -20,7 +20,7 @@ export const PsSheetHeader = ({ eyebrow, isLeadingDisabled = false, leadingKind
20
20
  minHeight: PsSheetConfig.headerMinHeight,
21
21
  paddingBottom: PsSheetConfig.headerPaddingBottom,
22
22
  paddingHorizontal: PsSheetConfig.headerPaddingHorizontal,
23
- paddingTop: PsSheetConfig.headerPaddingTop,
23
+ paddingTop: PsSheetService.headerPaddingTop(Platform.OS),
24
24
  }, testID: 'ps-sheet-header', children: [_jsx(View, { style: { alignItems: 'flex-start', minWidth: PsSheetConfig.actionMinWidth }, children: leadingLabel === undefined ? null : (_jsx(View, { style: PsSheetService.leadingIconStyle(leadingKind), children: _jsx(PsIconButton, { iconKey: leadingKind === 'back' ? PsIconKey.ChevronRight : PsIconKey.Close, isDisabled: isLeadingDisabled, label: leadingLabel, onPress: onLeadingPress, testID: 'ps-sheet-leading', variant: 'header' }) })) }), _jsxs(View, { style: { alignItems: 'center', flex: 1 }, children: [_jsx(PsText, { accessibilityRole: 'header', align: 'center', numberOfLines: 2, weight: 'bold', children: title }), eyebrow === undefined ? null : (_jsx(PsText, { align: 'center', numberOfLines: 1, tone: 'muted', variant: 'small', children: eyebrow }))] }), _jsx(View, { style: { alignItems: 'flex-end', minWidth: PsSheetConfig.actionMinWidth }, children: trailing === undefined ? null : trailing.isSubmitting === true ? (_jsx(View, { accessibilityLabel: trailing.label, accessibilityState: { busy: true }, style: {
25
25
  alignItems: 'center',
26
26
  backgroundColor: trailingColor,
@@ -1,4 +1,5 @@
1
1
  export declare const PsChartConfig: {
2
+ readonly revealDefaultToken: "default";
2
3
  readonly areaOpacity: 0.18;
3
4
  readonly axisLabelGap: 8;
4
5
  readonly axisLabelMaxWidth: 48;
@@ -1,4 +1,5 @@
1
1
  export const PsChartConfig = {
2
+ revealDefaultToken: 'default',
2
3
  areaOpacity: 0.18,
3
4
  axisLabelGap: 8,
4
5
  axisLabelMaxWidth: 48,
@@ -18,8 +18,8 @@ export declare const PsLayoutConfig: {
18
18
  readonly rowIconSize: 16;
19
19
  readonly scrollEventThrottle: 16;
20
20
  readonly selectMarkSize: 22;
21
- readonly rowMinHeight: 48;
22
- readonly rowMinHeightWithSubtitle: 62;
21
+ readonly menuRowMinHeight: 48;
22
+ readonly rowMinHeight: 62;
23
23
  readonly rowLineGap: 2;
24
24
  readonly rowPaddingVertical: 12;
25
25
  readonly sheetFooterGap: 8;
@@ -18,8 +18,8 @@ export const PsLayoutConfig = {
18
18
  rowIconSize: 16,
19
19
  scrollEventThrottle: 16,
20
20
  selectMarkSize: 22,
21
- rowMinHeight: 48,
22
- rowMinHeightWithSubtitle: 62,
21
+ menuRowMinHeight: 48,
22
+ rowMinHeight: 62,
23
23
  rowLineGap: 2,
24
24
  rowPaddingVertical: 12,
25
25
  sheetFooterGap: 8,
@@ -0,0 +1,19 @@
1
+ export declare const PsLinkPreviewConfig: {
2
+ readonly compactBodyPadding: 12;
3
+ readonly compactImageMinHeight: 80;
4
+ readonly compactImageWidth: 96;
5
+ readonly descriptionLines: 2;
6
+ readonly domainPrefixPattern: RegExp;
7
+ readonly faviconRadius: 2;
8
+ readonly faviconSize: 16;
9
+ readonly fullBodyPadding: 16;
10
+ readonly imageAspectRatio: 1.91;
11
+ readonly metaGap: 8;
12
+ readonly portSuffixPattern: RegExp;
13
+ readonly skeletonLineGap: 8;
14
+ readonly skeletonMetaWidth: 96;
15
+ readonly skeletonTitleWidth: 192;
16
+ readonly textGap: 4;
17
+ readonly titleLines: 2;
18
+ readonly webAddressPattern: RegExp;
19
+ };
@@ -0,0 +1,19 @@
1
+ export const PsLinkPreviewConfig = {
2
+ compactBodyPadding: 12,
3
+ compactImageMinHeight: 80,
4
+ compactImageWidth: 96,
5
+ descriptionLines: 2,
6
+ domainPrefixPattern: /^www\./i,
7
+ faviconRadius: 2,
8
+ faviconSize: 16,
9
+ fullBodyPadding: 16,
10
+ imageAspectRatio: 1.91,
11
+ metaGap: 8,
12
+ portSuffixPattern: /:\d*$/,
13
+ skeletonLineGap: 8,
14
+ skeletonMetaWidth: 96,
15
+ skeletonTitleWidth: 192,
16
+ textGap: 4,
17
+ titleLines: 2,
18
+ webAddressPattern: /^https?:\/\/([^/?#\s]+)/i,
19
+ };
@@ -0,0 +1,5 @@
1
+ export declare const PsLinkPreviewSize: {
2
+ readonly Compact: "compact";
3
+ readonly Full: "full";
4
+ };
5
+ export type PsLinkPreviewSize = (typeof PsLinkPreviewSize)[keyof typeof PsLinkPreviewSize];
@@ -0,0 +1,4 @@
1
+ export const PsLinkPreviewSize = {
2
+ Compact: 'compact',
3
+ Full: 'full',
4
+ };
@@ -8,4 +8,8 @@ export declare const PsMenuPopupConfig: {
8
8
  readonly remeasureMs: 250;
9
9
  readonly rowHeight: 48;
10
10
  readonly rowPaddingHorizontal: 16;
11
+ readonly tileFontSize: 14;
12
+ readonly tileLogoPadding: 3;
13
+ readonly tileRadius: 8;
14
+ readonly tileSize: 28;
11
15
  };
@@ -8,4 +8,8 @@ export const PsMenuPopupConfig = {
8
8
  remeasureMs: 250,
9
9
  rowHeight: 48,
10
10
  rowPaddingHorizontal: 16,
11
+ tileFontSize: 14,
12
+ tileLogoPadding: 3,
13
+ tileRadius: 8,
14
+ tileSize: 28,
11
15
  };
@@ -1,6 +1,7 @@
1
1
  export declare const PsSheetConfig: {
2
2
  readonly actionMinWidth: 44;
3
3
  readonly actionSpinnerGap: 6;
4
+ readonly androidHeaderPaddingTop: 0;
4
5
  readonly cornerRadius: 20;
5
6
  readonly formSizingMinIos: 18;
6
7
  readonly headerMinHeight: 64;
@@ -1,6 +1,7 @@
1
1
  export const PsSheetConfig = {
2
2
  actionMinWidth: 44,
3
3
  actionSpinnerGap: 6,
4
+ androidHeaderPaddingTop: 0,
4
5
  cornerRadius: 20,
5
6
  formSizingMinIos: 18,
6
7
  headerMinHeight: 64,
@@ -1,17 +1,21 @@
1
- import { useEffect } from 'react';
1
+ import { useEffect, useRef } from 'react';
2
2
  import { useSharedValue, withTiming } from 'react-native-reanimated';
3
3
  import { PsChartConfig } from '../constants/ps-chart-config';
4
+ import { PsChartRevealService } from '../services/ps-chart-reveal-service';
4
5
  import { usePsMobileTheme } from './use-ps-mobile-theme';
5
6
  export const usePsChartReveal = (isReady, revealKey) => {
6
7
  const { isReduceMotion: isReduced } = usePsMobileTheme();
7
8
  const progress = useSharedValue(isReduced ? 1 : 0);
9
+ const played = useRef(null);
8
10
  useEffect(() => {
9
11
  if (isReduced) {
10
12
  progress.set(1);
11
13
  return;
12
14
  }
13
- if (!isReady)
15
+ const token = PsChartRevealService.token(revealKey);
16
+ if (!PsChartRevealService.shouldPlay(isReady, played.current, token))
14
17
  return;
18
+ played.current = token;
15
19
  progress.set(0);
16
20
  progress.set(withTiming(1, { duration: PsChartConfig.drawMs }));
17
21
  }, [isReady, isReduced, progress, revealKey]);
package/dist/index.d.ts CHANGED
@@ -133,6 +133,9 @@ export * from './components/ps-date-range-field';
133
133
  export * from './components/ps-statement-table';
134
134
  export * from './components/ps-action-sheet';
135
135
  export * from './components/ps-card';
136
+ export * from './components/ps-link-preview-card';
137
+ export * from './constants/ps-link-preview-size';
138
+ export { PsLinkPreviewService } from './services/ps-link-preview-service';
136
139
  export * from './components/ps-checkbox';
137
140
  export * from './components/ps-chip';
138
141
  export * from './components/ps-icon-button';
package/dist/index.js CHANGED
@@ -128,6 +128,9 @@ export * from './components/ps-date-range-field';
128
128
  export * from './components/ps-statement-table';
129
129
  export * from './components/ps-action-sheet';
130
130
  export * from './components/ps-card';
131
+ export * from './components/ps-link-preview-card';
132
+ export * from './constants/ps-link-preview-size';
133
+ export { PsLinkPreviewService } from './services/ps-link-preview-service';
131
134
  export * from './components/ps-checkbox';
132
135
  export * from './components/ps-chip';
133
136
  export * from './components/ps-icon-button';
@@ -0,0 +1,5 @@
1
+ export interface PsLinkPreviewMeta {
2
+ hasDescription: boolean;
3
+ hasTitle: boolean;
4
+ siteLabel: string;
5
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -13,5 +13,6 @@ export interface PsMenuItem {
13
13
  monogram?: string;
14
14
  onSelect: () => void;
15
15
  subtitle?: string;
16
+ tileColor?: string;
16
17
  tone?: PsMenuItemTone;
17
18
  }
@@ -3,5 +3,6 @@ export interface PsMenuTileRequest {
3
3
  imageUri?: string;
4
4
  key: string;
5
5
  shape: PsMenuTileShape;
6
+ tileColor?: string;
6
7
  text: string;
7
8
  }
@@ -0,0 +1,4 @@
1
+ export declare class PsChartRevealService {
2
+ static token(revealKey: string | undefined): string;
3
+ static shouldPlay(isReady: boolean, played: string | null, token: string): boolean;
4
+ }
@@ -0,0 +1,9 @@
1
+ import { PsChartConfig } from '../constants/ps-chart-config';
2
+ export class PsChartRevealService {
3
+ static token(revealKey) {
4
+ return revealKey === undefined ? PsChartConfig.revealDefaultToken : `key:${revealKey}`;
5
+ }
6
+ static shouldPlay(isReady, played, token) {
7
+ return isReady && played !== token;
8
+ }
9
+ }
@@ -0,0 +1,12 @@
1
+ import { PsLinkPreviewSize } from '../constants/ps-link-preview-size';
2
+ import type { PsDocumentSource } from '../interfaces/ps-document-source';
3
+ import type { PsLinkPreviewMeta } from '../interfaces/ps-link-preview-meta';
4
+ export declare class PsLinkPreviewService {
5
+ static isWebAddress(href: string): boolean;
6
+ static domain(href: string): string;
7
+ static hasText(value: string | null | undefined): boolean;
8
+ static hasSource(source: PsDocumentSource | null | undefined, failedUri: string | null): boolean;
9
+ static isCompact(size: PsLinkPreviewSize): boolean;
10
+ static meta(href: string, title: string | null | undefined, siteName: string | null | undefined, description: string | null | undefined, size: PsLinkPreviewSize): PsLinkPreviewMeta;
11
+ static imageHeight(width: number): number;
12
+ }
@@ -0,0 +1,38 @@
1
+ import { PsLinkPreviewConfig } from '../constants/ps-link-preview-config';
2
+ import { PsLinkPreviewSize } from '../constants/ps-link-preview-size';
3
+ export class PsLinkPreviewService {
4
+ static isWebAddress(href) {
5
+ return PsLinkPreviewConfig.webAddressPattern.test(href);
6
+ }
7
+ static domain(href) {
8
+ const match = PsLinkPreviewConfig.webAddressPattern.exec(href);
9
+ if (match === null)
10
+ return href;
11
+ const authority = match[1];
12
+ const host = authority
13
+ .slice(authority.lastIndexOf('@') + 1)
14
+ .replace(PsLinkPreviewConfig.portSuffixPattern, '');
15
+ return host.replace(PsLinkPreviewConfig.domainPrefixPattern, '');
16
+ }
17
+ static hasText(value) {
18
+ return value !== null && value !== undefined && value.trim() !== '';
19
+ }
20
+ static hasSource(source, failedUri) {
21
+ return source !== null && source !== undefined && source.uri !== '' && source.uri !== failedUri;
22
+ }
23
+ static isCompact(size) {
24
+ return size === PsLinkPreviewSize.Compact;
25
+ }
26
+ static meta(href, title, siteName, description, size) {
27
+ return {
28
+ hasDescription: PsLinkPreviewService.hasText(description) && !PsLinkPreviewService.isCompact(size),
29
+ hasTitle: PsLinkPreviewService.hasText(title),
30
+ siteLabel: siteName !== null && siteName !== undefined && PsLinkPreviewService.hasText(siteName)
31
+ ? siteName
32
+ : PsLinkPreviewService.domain(href),
33
+ };
34
+ }
35
+ static imageHeight(width) {
36
+ return Math.round(width / PsLinkPreviewConfig.imageAspectRatio);
37
+ }
38
+ }
@@ -4,14 +4,14 @@ export class PsMenuTileService {
4
4
  if (!PsActionMenuService.hasAvatar(item))
5
5
  return null;
6
6
  const text = PsMenuTileService.letter(item.monogram === undefined ? item.label : item.monogram);
7
- return PsMenuTileService.request('rounded', item.imageUri, text);
7
+ return PsMenuTileService.request('rounded', item.imageUri, text, item.tileColor);
8
8
  }
9
9
  static headerRequest(header) {
10
10
  if (header === undefined)
11
11
  return null;
12
12
  if (header.imageUri === undefined && header.initials === undefined)
13
13
  return null;
14
- return PsMenuTileService.request('circle', header.imageUri, header.initials === undefined ? '' : header.initials.trim().toUpperCase());
14
+ return PsMenuTileService.request('circle', header.imageUri, header.initials === undefined ? '' : header.initials.trim().toUpperCase(), undefined);
15
15
  }
16
16
  static requests(header, sections) {
17
17
  const all = [
@@ -59,12 +59,18 @@ export class PsMenuTileService {
59
59
  return [item];
60
60
  return [item, ...item.children.flatMap((child) => PsMenuTileService.tree(child))];
61
61
  }
62
- static request(shape, imageUri, text) {
62
+ static request(shape, imageUri, text, tileColor) {
63
63
  return {
64
64
  imageUri,
65
- key: [shape, imageUri === undefined ? '' : imageUri, text].join('|'),
65
+ key: [
66
+ shape,
67
+ imageUri === undefined ? '' : imageUri,
68
+ text,
69
+ tileColor === undefined ? '' : tileColor,
70
+ ].join('|'),
66
71
  shape,
67
72
  text,
73
+ tileColor,
68
74
  };
69
75
  }
70
76
  }
@@ -7,6 +7,7 @@ export declare class PsSheetService {
7
7
  static fittedMaxWidth(windowWidth: number, layoutClass: PsLayoutClass): number | undefined;
8
8
  static supportsFormSizing(iosVersion: string | number): boolean;
9
9
  static handlesInset(os: string): boolean;
10
+ static headerPaddingTop(os: string): number;
10
11
  static bottomPadding(isFloating: boolean, bottomInset: number): number;
11
12
  static fittedHeight(headerHeight: number, bodyHeight: number, windowHeight: number, layoutClass?: PsLayoutClass): number;
12
13
  static raisedHeight(height: number, keyboardHeight: number, windowHeight: number, topInset: number): number;
@@ -20,6 +20,9 @@ export class PsSheetService {
20
20
  static handlesInset(os) {
21
21
  return os === 'ios' || os === 'android';
22
22
  }
23
+ static headerPaddingTop(os) {
24
+ return os === 'android' ? PsSheetConfig.androidHeaderPaddingTop : PsSheetConfig.headerPaddingTop;
25
+ }
23
26
  static bottomPadding(isFloating, bottomInset) {
24
27
  return PsLayoutConfig.sheetGutter + (isFloating ? 0 : bottomInset);
25
28
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pallasoft/ps-mobile-components",
3
- "version": "1.13.1",
3
+ "version": "1.15.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",