@solostylist/ui-kit-native 1.0.0 → 1.0.2
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/hooks/index.d.ts +6 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +5 -0
- package/dist/hooks/use-count-down.d.ts +72 -0
- package/dist/hooks/use-count-down.d.ts.map +1 -0
- package/dist/hooks/use-count-down.js +203 -0
- package/dist/hooks/use-is-passed-position.d.ts +20 -0
- package/dist/hooks/use-is-passed-position.d.ts.map +1 -0
- package/dist/hooks/use-is-passed-position.js +31 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +37 -0
- package/dist/s-avatar/index.d.ts +3 -0
- package/dist/s-avatar/index.d.ts.map +1 -0
- package/dist/s-avatar/index.js +1 -0
- package/dist/s-avatar/s-avatar.d.ts +43 -0
- package/dist/s-avatar/s-avatar.d.ts.map +1 -0
- package/dist/s-avatar/s-avatar.js +119 -0
- package/dist/s-button/index.d.ts +3 -0
- package/dist/s-button/index.d.ts.map +1 -0
- package/dist/s-button/index.js +1 -0
- package/dist/s-button/s-button.d.ts +55 -0
- package/dist/s-button/s-button.d.ts.map +1 -0
- package/dist/s-button/s-button.js +161 -0
- package/dist/s-button-link/index.d.ts +2 -0
- package/dist/s-button-link/index.d.ts.map +1 -0
- package/dist/s-button-link/index.js +1 -0
- package/dist/s-button-link/s-button-link.d.ts +43 -0
- package/dist/s-button-link/s-button-link.d.ts.map +1 -0
- package/dist/s-button-link/s-button-link.js +59 -0
- package/dist/s-chat-input/index.d.ts +2 -0
- package/dist/s-chat-input/index.d.ts.map +1 -0
- package/dist/s-chat-input/index.js +1 -0
- package/dist/s-chat-input/s-chat-input.d.ts +52 -0
- package/dist/s-chat-input/s-chat-input.d.ts.map +1 -0
- package/dist/s-chat-input/s-chat-input.js +135 -0
- package/dist/s-chat-message/index.d.ts +3 -0
- package/dist/s-chat-message/index.d.ts.map +1 -0
- package/dist/s-chat-message/index.js +2 -0
- package/dist/s-chat-message/s-chat-message.d.ts +61 -0
- package/dist/s-chat-message/s-chat-message.d.ts.map +1 -0
- package/dist/s-chat-message/s-chat-message.js +132 -0
- package/dist/s-checkbox/index.d.ts +3 -0
- package/dist/s-checkbox/index.d.ts.map +1 -0
- package/dist/s-checkbox/index.js +1 -0
- package/dist/s-checkbox/s-checkbox.d.ts +32 -0
- package/dist/s-checkbox/s-checkbox.d.ts.map +1 -0
- package/dist/s-checkbox/s-checkbox.js +52 -0
- package/dist/s-chip/index.d.ts +3 -0
- package/dist/s-chip/index.d.ts.map +1 -0
- package/dist/s-chip/index.js +1 -0
- package/dist/s-chip/s-chip.d.ts +52 -0
- package/dist/s-chip/s-chip.d.ts.map +1 -0
- package/dist/s-chip/s-chip.js +151 -0
- package/dist/s-code-block/index.d.ts +3 -0
- package/dist/s-code-block/index.d.ts.map +1 -0
- package/dist/s-code-block/index.js +2 -0
- package/dist/s-code-block/s-code-block.d.ts +40 -0
- package/dist/s-code-block/s-code-block.d.ts.map +1 -0
- package/dist/s-code-block/s-code-block.js +69 -0
- package/dist/s-comment-message/index.d.ts +3 -0
- package/dist/s-comment-message/index.d.ts.map +1 -0
- package/dist/s-comment-message/index.js +2 -0
- package/dist/s-comment-message/s-comment-message.d.ts +54 -0
- package/dist/s-comment-message/s-comment-message.d.ts.map +1 -0
- package/dist/s-comment-message/s-comment-message.js +109 -0
- package/dist/s-copyable-text/index.d.ts +3 -0
- package/dist/s-copyable-text/index.d.ts.map +1 -0
- package/dist/s-copyable-text/index.js +1 -0
- package/dist/s-copyable-text/s-copyable-text.d.ts +24 -0
- package/dist/s-copyable-text/s-copyable-text.d.ts.map +1 -0
- package/dist/s-copyable-text/s-copyable-text.js +52 -0
- package/dist/s-countdown/index.d.ts +7 -0
- package/dist/s-countdown/index.d.ts.map +1 -0
- package/dist/s-countdown/index.js +6 -0
- package/dist/s-countdown/s-count-box.d.ts +44 -0
- package/dist/s-countdown/s-count-box.d.ts.map +1 -0
- package/dist/s-countdown/s-count-box.js +135 -0
- package/dist/s-countdown/s-countdown.d.ts +72 -0
- package/dist/s-countdown/s-countdown.d.ts.map +1 -0
- package/dist/s-countdown/s-countdown.js +94 -0
- package/dist/s-data-table/index.d.ts +3 -0
- package/dist/s-data-table/index.d.ts.map +1 -0
- package/dist/s-data-table/index.js +2 -0
- package/dist/s-data-table/s-data-table.d.ts +64 -0
- package/dist/s-data-table/s-data-table.d.ts.map +1 -0
- package/dist/s-data-table/s-data-table.js +75 -0
- package/dist/s-date-picker/index.d.ts +3 -0
- package/dist/s-date-picker/index.d.ts.map +1 -0
- package/dist/s-date-picker/index.js +2 -0
- package/dist/s-date-picker/s-date-picker.d.ts +41 -0
- package/dist/s-date-picker/s-date-picker.d.ts.map +1 -0
- package/dist/s-date-picker/s-date-picker.js +161 -0
- package/dist/s-date-time-picker/index.d.ts +3 -0
- package/dist/s-date-time-picker/index.d.ts.map +1 -0
- package/dist/s-date-time-picker/index.js +2 -0
- package/dist/s-date-time-picker/s-date-time-picker.d.ts +47 -0
- package/dist/s-date-time-picker/s-date-time-picker.d.ts.map +1 -0
- package/dist/s-date-time-picker/s-date-time-picker.js +298 -0
- package/dist/s-divider/index.d.ts +2 -0
- package/dist/s-divider/index.d.ts.map +1 -0
- package/dist/s-divider/index.js +1 -0
- package/dist/s-divider/s-divider.d.ts +24 -0
- package/dist/s-divider/s-divider.d.ts.map +1 -0
- package/dist/s-divider/s-divider.js +30 -0
- package/dist/s-file-dropzone/index.d.ts +3 -0
- package/dist/s-file-dropzone/index.d.ts.map +1 -0
- package/dist/s-file-dropzone/index.js +2 -0
- package/dist/s-file-dropzone/s-file-dropzone.d.ts +63 -0
- package/dist/s-file-dropzone/s-file-dropzone.d.ts.map +1 -0
- package/dist/s-file-dropzone/s-file-dropzone.js +115 -0
- package/dist/s-file-icon/index.d.ts +3 -0
- package/dist/s-file-icon/index.d.ts.map +1 -0
- package/dist/s-file-icon/index.js +2 -0
- package/dist/s-file-icon/s-file-icon.d.ts +23 -0
- package/dist/s-file-icon/s-file-icon.d.ts.map +1 -0
- package/dist/s-file-icon/s-file-icon.js +86 -0
- package/dist/s-form/s-form.d.ts +0 -1
- package/dist/s-form/s-form.d.ts.map +1 -1
- package/dist/s-form/s-form.js +0 -1
- package/dist/s-icon-button/s-icon-button.d.ts +9 -9
- package/dist/s-icon-button/s-icon-button.d.ts.map +1 -1
- package/dist/s-icon-button/s-icon-button.js +38 -4
- package/dist/s-image-comparison/index.d.ts +2 -0
- package/dist/s-image-comparison/index.d.ts.map +1 -0
- package/dist/s-image-comparison/index.js +1 -0
- package/dist/s-image-comparison/s-image-comparison.d.ts +35 -0
- package/dist/s-image-comparison/s-image-comparison.d.ts.map +1 -0
- package/dist/s-image-comparison/s-image-comparison.js +111 -0
- package/dist/s-label/index.d.ts +3 -0
- package/dist/s-label/index.d.ts.map +1 -0
- package/dist/s-label/index.js +1 -0
- package/dist/s-label/s-label.d.ts +24 -0
- package/dist/s-label/s-label.d.ts.map +1 -0
- package/dist/s-label/s-label.js +19 -0
- package/dist/s-language-switcher/index.d.ts +4 -0
- package/dist/s-language-switcher/index.d.ts.map +1 -0
- package/dist/s-language-switcher/index.js +2 -0
- package/dist/s-language-switcher/s-language-switcher.d.ts +53 -0
- package/dist/s-language-switcher/s-language-switcher.d.ts.map +1 -0
- package/dist/s-language-switcher/s-language-switcher.js +57 -0
- package/dist/s-lazy-image/index.d.ts +3 -0
- package/dist/s-lazy-image/index.d.ts.map +1 -0
- package/dist/s-lazy-image/index.js +1 -0
- package/dist/s-lazy-image/s-lazy-image.d.ts +43 -0
- package/dist/s-lazy-image/s-lazy-image.d.ts.map +1 -0
- package/dist/s-lazy-image/s-lazy-image.js +74 -0
- package/dist/s-moving-border/index.d.ts +2 -0
- package/dist/s-moving-border/index.d.ts.map +1 -0
- package/dist/s-moving-border/index.js +1 -0
- package/dist/s-moving-border/s-moving-border.d.ts +38 -0
- package/dist/s-moving-border/s-moving-border.d.ts.map +1 -0
- package/dist/s-moving-border/s-moving-border.js +153 -0
- package/dist/s-multi-select/index.d.ts +2 -0
- package/dist/s-multi-select/index.d.ts.map +1 -0
- package/dist/s-multi-select/index.js +1 -0
- package/dist/s-multi-select/s-multi-select.d.ts +53 -0
- package/dist/s-multi-select/s-multi-select.d.ts.map +1 -0
- package/dist/s-multi-select/s-multi-select.js +150 -0
- package/dist/s-pagination/index.d.ts +3 -0
- package/dist/s-pagination/index.d.ts.map +1 -0
- package/dist/s-pagination/index.js +2 -0
- package/dist/s-pagination/s-pagination.d.ts +49 -0
- package/dist/s-pagination/s-pagination.d.ts.map +1 -0
- package/dist/s-pagination/s-pagination.js +147 -0
- package/dist/s-rating/index.d.ts +3 -0
- package/dist/s-rating/index.d.ts.map +1 -0
- package/dist/s-rating/index.js +1 -0
- package/dist/s-rating/s-rating.d.ts +55 -0
- package/dist/s-rating/s-rating.d.ts.map +1 -0
- package/dist/s-rating/s-rating.js +157 -0
- package/dist/s-review/index.d.ts +3 -0
- package/dist/s-review/index.d.ts.map +1 -0
- package/dist/s-review/index.js +1 -0
- package/dist/s-review/s-review.d.ts +55 -0
- package/dist/s-review/s-review.d.ts.map +1 -0
- package/dist/s-review/s-review.js +148 -0
- package/dist/s-scroll-to-top/index.d.ts +3 -0
- package/dist/s-scroll-to-top/index.d.ts.map +1 -0
- package/dist/s-scroll-to-top/index.js +2 -0
- package/dist/s-scroll-to-top/s-scroll-to-top.d.ts +36 -0
- package/dist/s-scroll-to-top/s-scroll-to-top.d.ts.map +1 -0
- package/dist/s-scroll-to-top/s-scroll-to-top.js +61 -0
- package/dist/s-select/s-select.d.ts +1 -3
- package/dist/s-select/s-select.d.ts.map +1 -1
- package/dist/s-select/s-select.js +61 -101
- package/dist/s-select-list/index.d.ts +3 -0
- package/dist/s-select-list/index.d.ts.map +1 -0
- package/dist/s-select-list/index.js +2 -0
- package/dist/s-select-list/s-select-list.d.ts +34 -0
- package/dist/s-select-list/s-select-list.d.ts.map +1 -0
- package/dist/s-select-list/s-select-list.js +115 -0
- package/dist/s-skeleton/index.d.ts +3 -0
- package/dist/s-skeleton/index.d.ts.map +1 -0
- package/dist/s-skeleton/index.js +1 -0
- package/dist/s-skeleton/s-skeleton.d.ts +27 -0
- package/dist/s-skeleton/s-skeleton.d.ts.map +1 -0
- package/dist/s-skeleton/s-skeleton.js +126 -0
- package/dist/s-switch/index.d.ts +3 -0
- package/dist/s-switch/index.d.ts.map +1 -0
- package/dist/s-switch/index.js +1 -0
- package/dist/s-switch/s-switch.d.ts +30 -0
- package/dist/s-switch/s-switch.d.ts.map +1 -0
- package/dist/s-switch/s-switch.js +44 -0
- package/dist/s-text-field/s-text-field.d.ts +0 -2
- package/dist/s-text-field/s-text-field.d.ts.map +1 -1
- package/dist/s-text-field/s-text-field.js +10 -23
- package/dist/s-text-shimmer/index.d.ts +2 -0
- package/dist/s-text-shimmer/index.d.ts.map +1 -0
- package/dist/s-text-shimmer/index.js +1 -0
- package/dist/s-text-shimmer/s-text-shimmer.d.ts +22 -0
- package/dist/s-text-shimmer/s-text-shimmer.d.ts.map +1 -0
- package/dist/s-text-shimmer/s-text-shimmer.js +102 -0
- package/dist/s-text-truncation/index.d.ts +2 -0
- package/dist/s-text-truncation/index.d.ts.map +1 -0
- package/dist/s-text-truncation/index.js +1 -0
- package/dist/s-text-truncation/s-text-truncation.d.ts +42 -0
- package/dist/s-text-truncation/s-text-truncation.d.ts.map +1 -0
- package/dist/s-text-truncation/s-text-truncation.js +88 -0
- package/dist/s-tip/index.d.ts +3 -0
- package/dist/s-tip/index.d.ts.map +1 -0
- package/dist/s-tip/index.js +1 -0
- package/dist/s-tip/s-tip.d.ts +17 -0
- package/dist/s-tip/s-tip.d.ts.map +1 -0
- package/dist/s-tip/s-tip.js +25 -0
- package/dist/s-tooltip/index.d.ts +2 -0
- package/dist/s-tooltip/index.d.ts.map +1 -0
- package/dist/s-tooltip/index.js +1 -0
- package/dist/s-tooltip/s-tooltip.d.ts +23 -0
- package/dist/s-tooltip/s-tooltip.d.ts.map +1 -0
- package/dist/s-tooltip/s-tooltip.js +17 -0
- package/dist/s-zoom-image/index.d.ts +3 -0
- package/dist/s-zoom-image/index.d.ts.map +1 -0
- package/dist/s-zoom-image/index.js +1 -0
- package/dist/s-zoom-image/s-zoom-image.d.ts +38 -0
- package/dist/s-zoom-image/s-zoom-image.d.ts.map +1 -0
- package/dist/s-zoom-image/s-zoom-image.js +149 -0
- package/dist/theme/theme-primitives.d.ts +10 -0
- package/dist/theme/theme-primitives.d.ts.map +1 -1
- package/dist/theme/theme-primitives.js +11 -0
- package/dist/utils/bytes-to-size.d.ts +9 -0
- package/dist/utils/bytes-to-size.d.ts.map +1 -0
- package/dist/utils/bytes-to-size.js +17 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +1 -0
- package/package.json +175 -5
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo } from 'react';
|
|
3
|
+
import { View } from 'react-native';
|
|
4
|
+
import { SButton } from '../s-button';
|
|
5
|
+
import { SIconButton } from '../s-icon-button';
|
|
6
|
+
import { SSkeleton } from '../s-skeleton';
|
|
7
|
+
import { SText } from '../s-text';
|
|
8
|
+
import { useSTheme } from '../theme';
|
|
9
|
+
/**
|
|
10
|
+
* Skeleton loading component for SPagination.
|
|
11
|
+
* Matches the web SPagination skeleton layout: [icon-button] [rounded bar] [icon-button]
|
|
12
|
+
*/
|
|
13
|
+
const SPaginationSkeleton = ({ size = 'medium', style, }) => {
|
|
14
|
+
// Icon button size matches web SSkeleton component="icon-button" (32px circular)
|
|
15
|
+
const iconSize = size === 'small' ? 26 : 32;
|
|
16
|
+
// Bar width matches web SSkeleton variant="rounded" width={120}
|
|
17
|
+
const barWidth = size === 'small' ? 90 : 120;
|
|
18
|
+
const barHeight = size === 'small' ? 26 : 32;
|
|
19
|
+
return (_jsxs(View, { style: [
|
|
20
|
+
{
|
|
21
|
+
flexDirection: 'row',
|
|
22
|
+
alignItems: 'center',
|
|
23
|
+
justifyContent: 'center',
|
|
24
|
+
gap: 8,
|
|
25
|
+
},
|
|
26
|
+
style,
|
|
27
|
+
], children: [_jsx(SSkeleton, { variant: "circular", width: iconSize, height: iconSize }), _jsx(SSkeleton, { variant: "rounded", width: barWidth, height: barHeight }), _jsx(SSkeleton, { variant: "circular", width: iconSize, height: iconSize })] }));
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Generate array of page numbers to display
|
|
31
|
+
*/
|
|
32
|
+
const generatePageNumbers = (currentPage, totalPages, siblingCount, boundaryCount) => {
|
|
33
|
+
// Calculate total number of page buttons including ellipsis
|
|
34
|
+
const totalButtons = siblingCount * 2 + 1 + boundaryCount * 2 + 2; // +2 for potential ellipsis
|
|
35
|
+
// If total pages is less than total buttons, show all pages
|
|
36
|
+
if (totalPages <= totalButtons) {
|
|
37
|
+
return Array.from({ length: totalPages }, (_, i) => i + 1);
|
|
38
|
+
}
|
|
39
|
+
const pages = [];
|
|
40
|
+
// Always show first boundaryCount pages
|
|
41
|
+
for (let i = 1; i <= boundaryCount; i++) {
|
|
42
|
+
pages.push(i);
|
|
43
|
+
}
|
|
44
|
+
// Calculate range around current page
|
|
45
|
+
const leftSiblingIndex = Math.max(currentPage - siblingCount, boundaryCount + 1);
|
|
46
|
+
const rightSiblingIndex = Math.min(currentPage + siblingCount, totalPages - boundaryCount);
|
|
47
|
+
// Add ellipsis before current page range if needed
|
|
48
|
+
if (leftSiblingIndex > boundaryCount + 2) {
|
|
49
|
+
pages.push('ellipsis');
|
|
50
|
+
}
|
|
51
|
+
else if (leftSiblingIndex === boundaryCount + 2) {
|
|
52
|
+
pages.push(boundaryCount + 1);
|
|
53
|
+
}
|
|
54
|
+
// Add current page and siblings
|
|
55
|
+
for (let i = leftSiblingIndex; i <= rightSiblingIndex; i++) {
|
|
56
|
+
if (i > boundaryCount && i <= totalPages - boundaryCount) {
|
|
57
|
+
pages.push(i);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// Add ellipsis after current page range if needed
|
|
61
|
+
if (rightSiblingIndex < totalPages - boundaryCount - 1) {
|
|
62
|
+
pages.push('ellipsis');
|
|
63
|
+
}
|
|
64
|
+
else if (rightSiblingIndex === totalPages - boundaryCount - 1) {
|
|
65
|
+
pages.push(totalPages - boundaryCount);
|
|
66
|
+
}
|
|
67
|
+
// Always show last boundaryCount pages
|
|
68
|
+
for (let i = totalPages - boundaryCount + 1; i <= totalPages; i++) {
|
|
69
|
+
if (!pages.includes(i)) {
|
|
70
|
+
pages.push(i);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return pages;
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* Get button dimensions based on size
|
|
77
|
+
*/
|
|
78
|
+
const getButtonDimensions = (size) => {
|
|
79
|
+
switch (size) {
|
|
80
|
+
case 'small':
|
|
81
|
+
return { minWidth: 32, height: 32, gap: 4, fontSize: 12 };
|
|
82
|
+
case 'medium':
|
|
83
|
+
return { minWidth: 40, height: 40, gap: 8, fontSize: 14 };
|
|
84
|
+
case 'large':
|
|
85
|
+
return { minWidth: 48, height: 48, gap: 8, fontSize: 16 };
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* Enhanced pagination component with responsive sizing and loading states.
|
|
90
|
+
* Built on React Native Paper components with consistent theming.
|
|
91
|
+
*/
|
|
92
|
+
export const SPagination = ({ count = 0, page = 1, loading = false, onChange, size = 'medium', color = 'primary', variant = 'text', shape = 'circular', showFirstButton = false, showLastButton = false, hidePrevButton = false, hideNextButton = false, disabled = false, siblingCount = 1, boundaryCount = 1, style, }) => {
|
|
93
|
+
const { theme } = useSTheme();
|
|
94
|
+
// All hooks must be called before any conditional returns
|
|
95
|
+
const pageNumbers = useMemo(() => generatePageNumbers(page, count, siblingCount, boundaryCount), [page, count, siblingCount, boundaryCount]);
|
|
96
|
+
const buttonSize = size === 'large' ? 'large' : size === 'small' ? 'small' : 'medium';
|
|
97
|
+
const dimensions = getButtonDimensions(size);
|
|
98
|
+
// Shape-based border radius: circular = fully round, rounded = slightly rounded
|
|
99
|
+
const borderRadius = shape === 'circular' ? dimensions.height / 2 : theme.borderRadius.md;
|
|
100
|
+
const handlePageChange = (newPage) => {
|
|
101
|
+
if (onChange && newPage !== page && newPage >= 1 && newPage <= count) {
|
|
102
|
+
onChange({}, newPage);
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
// Hide pagination when there are no pages to display
|
|
106
|
+
if (count === 0) {
|
|
107
|
+
return _jsx(_Fragment, {});
|
|
108
|
+
}
|
|
109
|
+
// Show skeleton loading state while data is being fetched
|
|
110
|
+
if (loading) {
|
|
111
|
+
return _jsx(SPaginationSkeleton, { size: size, style: style });
|
|
112
|
+
}
|
|
113
|
+
// Determine inactive button variant based on pagination variant prop
|
|
114
|
+
// 'text': inactive pages have no border (text style)
|
|
115
|
+
// 'outlined': inactive pages have outlined border
|
|
116
|
+
const inactiveVariant = variant === 'outlined' ? 'outlined' : 'text';
|
|
117
|
+
return (_jsxs(View, { style: [
|
|
118
|
+
{
|
|
119
|
+
flexDirection: 'row',
|
|
120
|
+
alignItems: 'center',
|
|
121
|
+
justifyContent: 'center',
|
|
122
|
+
flexWrap: 'wrap',
|
|
123
|
+
gap: dimensions.gap,
|
|
124
|
+
},
|
|
125
|
+
style,
|
|
126
|
+
], children: [showFirstButton && (_jsx(SIconButton, { icon: "page-first", size: buttonSize, disabled: disabled || page === 1, onPress: () => handlePageChange(1) })), !hidePrevButton && (_jsx(SIconButton, { icon: "chevron-left", size: buttonSize, disabled: disabled || page === 1, onPress: () => handlePageChange(page - 1) })), pageNumbers.map((pageNum, index) => {
|
|
127
|
+
if (pageNum === 'ellipsis') {
|
|
128
|
+
return (_jsx(View, { style: {
|
|
129
|
+
paddingHorizontal: size === 'small' ? 4 : 8,
|
|
130
|
+
justifyContent: 'center',
|
|
131
|
+
alignItems: 'center',
|
|
132
|
+
}, children: _jsx(SText, { variant: "body2", children: "..." }) }, `ellipsis-${index}`));
|
|
133
|
+
}
|
|
134
|
+
const isActive = pageNum === page;
|
|
135
|
+
return (_jsx(SButton, { variant: isActive ? 'contained' : inactiveVariant, color: color, size: buttonSize, disabled: disabled, onPress: () => handlePageChange(pageNum), style: [
|
|
136
|
+
{
|
|
137
|
+
minWidth: 0,
|
|
138
|
+
width: dimensions.minWidth,
|
|
139
|
+
height: dimensions.height,
|
|
140
|
+
borderRadius,
|
|
141
|
+
},
|
|
142
|
+
!isActive && variant === 'outlined' && { borderColor: theme.colors.divider },
|
|
143
|
+
], contentStyle: { paddingHorizontal: 0 }, labelStyle: [{ marginHorizontal: 0 }, !isActive ? { color: theme.colors.text.primary } : undefined], children: String(pageNum) }, pageNum));
|
|
144
|
+
}), !hideNextButton && (_jsx(SIconButton, { icon: "chevron-right", size: buttonSize, disabled: disabled || page === count, onPress: () => handlePageChange(page + 1) })), showLastButton && (_jsx(SIconButton, { icon: "page-last", size: buttonSize, disabled: disabled || page === count, onPress: () => handlePageChange(count) }))] }));
|
|
145
|
+
};
|
|
146
|
+
SPagination.displayName = 'SPagination';
|
|
147
|
+
export default SPagination;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/s-rating/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAC9C,YAAY,EAAE,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { SRating, default } from './s-rating';
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { type StyleProp, type ViewStyle } from 'react-native';
|
|
3
|
+
/**
|
|
4
|
+
* Rating size variants - matches web SRating from @solostylist/ui-kit
|
|
5
|
+
*/
|
|
6
|
+
export type SRatingSize = 'small' | 'medium' | 'large';
|
|
7
|
+
/**
|
|
8
|
+
* Rating color variants
|
|
9
|
+
*/
|
|
10
|
+
export type SRatingColor = 'default' | 'primary' | 'warning';
|
|
11
|
+
/**
|
|
12
|
+
* Props interface for SRating component
|
|
13
|
+
*/
|
|
14
|
+
export interface SRatingProps {
|
|
15
|
+
/** Current rating value */
|
|
16
|
+
value?: number | null;
|
|
17
|
+
/** Default value for uncontrolled component */
|
|
18
|
+
defaultValue?: number;
|
|
19
|
+
/** Maximum rating value (number of stars) */
|
|
20
|
+
max?: number;
|
|
21
|
+
/** Size of the rating icons */
|
|
22
|
+
size?: SRatingSize;
|
|
23
|
+
/** Color variant of the rating */
|
|
24
|
+
color?: SRatingColor;
|
|
25
|
+
/** Whether the rating is read-only */
|
|
26
|
+
readOnly?: boolean;
|
|
27
|
+
/** Whether the rating is disabled */
|
|
28
|
+
disabled?: boolean;
|
|
29
|
+
/** Precision of rating (0.5 for half stars, 1 for full stars) */
|
|
30
|
+
precision?: 0.5 | 1;
|
|
31
|
+
/** Callback fired when the rating changes */
|
|
32
|
+
onChange?: (event: any, value: number | null) => void;
|
|
33
|
+
/** Callback fired when hover state changes */
|
|
34
|
+
onChangeActive?: (event: any, value: number) => void;
|
|
35
|
+
/** Custom filled icon name (Material Community Icons) */
|
|
36
|
+
icon?: string;
|
|
37
|
+
/** Custom empty icon name (Material Community Icons) */
|
|
38
|
+
emptyIcon?: string;
|
|
39
|
+
/** Label text for accessibility */
|
|
40
|
+
name?: string;
|
|
41
|
+
/** Additional styles for the container */
|
|
42
|
+
style?: StyleProp<ViewStyle>;
|
|
43
|
+
/** Whether to highlight only the selected icon (vs all icons up to selected) */
|
|
44
|
+
highlightSelectedOnly?: boolean;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* A star rating component that allows users to provide feedback and ratings.
|
|
48
|
+
* Built with custom implementation for React Native with consistent theming.
|
|
49
|
+
*/
|
|
50
|
+
export declare const SRating: {
|
|
51
|
+
({ value: controlledValue, defaultValue, max, size, color, readOnly, disabled, precision, onChange, onChangeActive, icon, emptyIcon, name, style, highlightSelectedOnly, }: SRatingProps): React.JSX.Element;
|
|
52
|
+
displayName: string;
|
|
53
|
+
};
|
|
54
|
+
export default SRating;
|
|
55
|
+
//# sourceMappingURL=s-rating.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"s-rating.d.ts","sourceRoot":"","sources":["../../src/s-rating/s-rating.tsx"],"names":[],"mappings":"AACA,OAAO,KAA4B,MAAM,OAAO,CAAC;AACjD,OAAO,EAA0B,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAItF;;GAEG;AACH,MAAM,MAAM,WAAW,GAAG,OAAO,GAAG,QAAQ,GAAG,OAAO,CAAC;AAEvD;;GAEG;AACH,MAAM,MAAM,YAAY,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,CAAC;AAE7D;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,2BAA2B;IAC3B,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,+CAA+C;IAC/C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,6CAA6C;IAC7C,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,+BAA+B;IAC/B,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB,kCAAkC;IAClC,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB,sCAAsC;IACtC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,qCAAqC;IACrC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,iEAAiE;IACjE,SAAS,CAAC,EAAE,GAAG,GAAG,CAAC,CAAC;IACpB,6CAA6C;IAC7C,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IACtD,8CAA8C;IAC9C,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACrD,yDAAyD;IACzD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,wDAAwD;IACxD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mCAAmC;IACnC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,0CAA0C;IAC1C,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,gFAAgF;IAChF,qBAAqB,CAAC,EAAE,OAAO,CAAC;CACjC;AAkCD;;;GAGG;AACH,eAAO,MAAM,OAAO;gLAgBjB,YAAY,GAAG,KAAK,CAAC,GAAG,CAAC,OAAO;;CAwNlC,CAAC;AAIF,eAAe,OAAO,CAAC"}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
|
3
|
+
import { useMemo, useState } from 'react';
|
|
4
|
+
import { TouchableOpacity, View } from 'react-native';
|
|
5
|
+
import { brand, gray, lightBrand, lightGray, lightOrange, orange } from '@solostylist/core';
|
|
6
|
+
import { useSTheme } from '../theme';
|
|
7
|
+
/**
|
|
8
|
+
* Get color palettes for light and dark modes
|
|
9
|
+
*/
|
|
10
|
+
const getColorPalettes = (color) => {
|
|
11
|
+
switch (color) {
|
|
12
|
+
case 'default':
|
|
13
|
+
return { light: lightGray, dark: gray };
|
|
14
|
+
case 'primary':
|
|
15
|
+
return { light: lightBrand, dark: brand };
|
|
16
|
+
case 'warning':
|
|
17
|
+
return { light: lightOrange, dark: orange };
|
|
18
|
+
default:
|
|
19
|
+
return { light: lightOrange, dark: orange };
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Get icon size based on rating size
|
|
24
|
+
*/
|
|
25
|
+
const getIconSize = (size) => {
|
|
26
|
+
switch (size) {
|
|
27
|
+
case 'small':
|
|
28
|
+
return 18;
|
|
29
|
+
case 'medium':
|
|
30
|
+
return 24;
|
|
31
|
+
case 'large':
|
|
32
|
+
return 32;
|
|
33
|
+
default:
|
|
34
|
+
return 24;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* A star rating component that allows users to provide feedback and ratings.
|
|
39
|
+
* Built with custom implementation for React Native with consistent theming.
|
|
40
|
+
*/
|
|
41
|
+
export const SRating = ({ value: controlledValue, defaultValue = 0, max = 5, size = 'medium', color = 'warning', readOnly = false, disabled = false, precision = 1, onChange, onChangeActive, icon = 'star', emptyIcon = 'star-outline', name, style, highlightSelectedOnly = false, }) => {
|
|
42
|
+
const { theme } = useSTheme();
|
|
43
|
+
const [internalValue, setInternalValue] = useState(defaultValue);
|
|
44
|
+
const [hoverValue, setHoverValue] = useState(null);
|
|
45
|
+
// Use controlled value if provided, otherwise use internal state
|
|
46
|
+
const currentValue = controlledValue !== undefined ? controlledValue : internalValue;
|
|
47
|
+
const displayValue = hoverValue !== null ? hoverValue : (currentValue ?? 0);
|
|
48
|
+
const { light: lightColor, dark: darkColor } = getColorPalettes(color);
|
|
49
|
+
const colorPalette = theme.dark ? darkColor : lightColor;
|
|
50
|
+
const iconSize = getIconSize(size);
|
|
51
|
+
// Calculate colors based on theme mode
|
|
52
|
+
const colors = useMemo(() => {
|
|
53
|
+
const isDark = theme.dark;
|
|
54
|
+
// Filled color - matches web rating styling (golden/amber for default)
|
|
55
|
+
const filledColor = isDark ? colorPalette[400] : colorPalette[600];
|
|
56
|
+
// Empty/unfilled color
|
|
57
|
+
const emptyColor = isDark ? gray[600] : lightGray[400];
|
|
58
|
+
// Disabled color
|
|
59
|
+
const disabledColor = isDark ? gray[700] : lightGray[300];
|
|
60
|
+
return {
|
|
61
|
+
filledColor,
|
|
62
|
+
emptyColor,
|
|
63
|
+
disabledColor,
|
|
64
|
+
};
|
|
65
|
+
}, [theme.dark, colorPalette]);
|
|
66
|
+
const handlePress = (index, isHalf = false) => {
|
|
67
|
+
if (readOnly || disabled)
|
|
68
|
+
return;
|
|
69
|
+
let newValue;
|
|
70
|
+
if (precision === 0.5 && isHalf) {
|
|
71
|
+
newValue = index + 0.5;
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
newValue = index + 1;
|
|
75
|
+
}
|
|
76
|
+
// Allow deselecting by clicking the same value
|
|
77
|
+
if (newValue === currentValue) {
|
|
78
|
+
newValue = 0;
|
|
79
|
+
}
|
|
80
|
+
if (controlledValue === undefined) {
|
|
81
|
+
setInternalValue(newValue);
|
|
82
|
+
}
|
|
83
|
+
onChange?.(null, newValue === 0 ? null : newValue);
|
|
84
|
+
};
|
|
85
|
+
const handleHoverEnter = (index, isHalf = false) => {
|
|
86
|
+
if (readOnly || disabled)
|
|
87
|
+
return;
|
|
88
|
+
let hoverVal;
|
|
89
|
+
if (precision === 0.5 && isHalf) {
|
|
90
|
+
hoverVal = index + 0.5;
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
hoverVal = index + 1;
|
|
94
|
+
}
|
|
95
|
+
setHoverValue(hoverVal);
|
|
96
|
+
onChangeActive?.(null, hoverVal);
|
|
97
|
+
};
|
|
98
|
+
const handleHoverLeave = () => {
|
|
99
|
+
if (readOnly || disabled)
|
|
100
|
+
return;
|
|
101
|
+
setHoverValue(null);
|
|
102
|
+
onChangeActive?.(null, -1);
|
|
103
|
+
};
|
|
104
|
+
const renderIcon = (index) => {
|
|
105
|
+
const isFilled = highlightSelectedOnly ? displayValue === index + 1 : displayValue >= index + 1;
|
|
106
|
+
const isHalfFilled = !highlightSelectedOnly && precision === 0.5 && displayValue === index + 0.5;
|
|
107
|
+
const iconColor = disabled
|
|
108
|
+
? colors.disabledColor
|
|
109
|
+
: isFilled || isHalfFilled
|
|
110
|
+
? colors.filledColor
|
|
111
|
+
: colors.emptyColor;
|
|
112
|
+
const iconName = isFilled ? icon : emptyIcon;
|
|
113
|
+
if (precision === 0.5) {
|
|
114
|
+
// Render half-star support
|
|
115
|
+
return (_jsxs(View, { style: {
|
|
116
|
+
position: 'relative',
|
|
117
|
+
}, children: [_jsx(TouchableOpacity, { onPress: () => handlePress(index, false), onPressIn: () => handleHoverEnter(index, false), onPressOut: handleHoverLeave, disabled: readOnly || disabled, activeOpacity: readOnly || disabled ? 1 : 0.7, accessibilityRole: "button", accessibilityLabel: name ? `${name} ${index + 1} stars` : `${index + 1} stars`, accessibilityState: { disabled: disabled || readOnly }, children: _jsxs(View, { style: {
|
|
118
|
+
flexDirection: 'row',
|
|
119
|
+
overflow: 'hidden',
|
|
120
|
+
}, children: [_jsx(MaterialCommunityIcons, { name: emptyIcon, size: iconSize, color: disabled ? colors.disabledColor : colors.emptyColor }), isHalfFilled && (_jsx(View, { style: {
|
|
121
|
+
position: 'absolute',
|
|
122
|
+
left: 0,
|
|
123
|
+
top: 0,
|
|
124
|
+
width: iconSize / 2,
|
|
125
|
+
overflow: 'hidden',
|
|
126
|
+
}, children: _jsx(MaterialCommunityIcons, { name: icon, size: iconSize, color: disabled ? colors.disabledColor : colors.filledColor }) })), isFilled && (_jsx(View, { style: [
|
|
127
|
+
{
|
|
128
|
+
position: 'absolute',
|
|
129
|
+
left: 0,
|
|
130
|
+
top: 0,
|
|
131
|
+
width: iconSize / 2,
|
|
132
|
+
overflow: 'hidden',
|
|
133
|
+
},
|
|
134
|
+
{ width: iconSize },
|
|
135
|
+
], children: _jsx(MaterialCommunityIcons, { name: icon, size: iconSize, color: disabled ? colors.disabledColor : colors.filledColor }) }))] }) }), _jsx(TouchableOpacity, { style: {
|
|
136
|
+
position: 'absolute',
|
|
137
|
+
left: 0,
|
|
138
|
+
top: 0,
|
|
139
|
+
width: iconSize / 2,
|
|
140
|
+
height: iconSize,
|
|
141
|
+
}, onPress: () => handlePress(index, true), onPressIn: () => handleHoverEnter(index, true), onPressOut: handleHoverLeave, disabled: readOnly || disabled, activeOpacity: 0, accessibilityLabel: name ? `${name} ${index + 0.5} stars` : `${index + 0.5} stars` })] }, index));
|
|
142
|
+
}
|
|
143
|
+
// Full star only (precision = 1)
|
|
144
|
+
return (_jsx(TouchableOpacity, { onPress: () => handlePress(index), onPressIn: () => handleHoverEnter(index), onPressOut: handleHoverLeave, disabled: readOnly || disabled, activeOpacity: readOnly || disabled ? 1 : 0.7, accessibilityRole: "button", accessibilityLabel: name ? `${name} ${index + 1} stars` : `${index + 1} stars`, accessibilityState: { disabled: disabled || readOnly }, children: _jsx(MaterialCommunityIcons, { name: iconName, size: iconSize, color: iconColor }) }, index));
|
|
145
|
+
};
|
|
146
|
+
return (_jsx(View, { style: [
|
|
147
|
+
{
|
|
148
|
+
flexDirection: 'row',
|
|
149
|
+
alignItems: 'center',
|
|
150
|
+
gap: 2,
|
|
151
|
+
opacity: disabled ? 0.5 : 1,
|
|
152
|
+
},
|
|
153
|
+
style,
|
|
154
|
+
], accessibilityRole: "adjustable", accessibilityLabel: name, children: Array.from({ length: max }, (_, index) => renderIcon(index)) }));
|
|
155
|
+
};
|
|
156
|
+
SRating.displayName = 'SRating';
|
|
157
|
+
export default SRating;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/s-review/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAC9C,YAAY,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { SReview, default } from './s-review';
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { type ImageSourcePropType, type StyleProp, type ViewStyle } from 'react-native';
|
|
3
|
+
/**
|
|
4
|
+
* Props interface for SReview component
|
|
5
|
+
*/
|
|
6
|
+
export interface SReviewProps {
|
|
7
|
+
/** Unique identifier for the review (default: auto-generated) */
|
|
8
|
+
id?: string | number;
|
|
9
|
+
/** Name of the reviewer */
|
|
10
|
+
userName: string;
|
|
11
|
+
/** URL or local asset for the user avatar image (falls back to initials if not provided) */
|
|
12
|
+
userAvatar?: string | ImageSourcePropType;
|
|
13
|
+
/** Rating value */
|
|
14
|
+
rating: number;
|
|
15
|
+
/** The review text content */
|
|
16
|
+
reviewContent: string;
|
|
17
|
+
/** Date and time of the review (automatically converts to relative time) */
|
|
18
|
+
datetime: Date | string;
|
|
19
|
+
/** Array of image URLs or local assets to display with the review */
|
|
20
|
+
images?: (string | ImageSourcePropType)[];
|
|
21
|
+
/** Current user vote status for this review (default: null) */
|
|
22
|
+
userHelpfulVote?: 'yes' | 'no' | null;
|
|
23
|
+
/** Number of "Yes" votes for helpfulness (default: 0) */
|
|
24
|
+
helpfulYes?: number;
|
|
25
|
+
/** Number of "No" votes for helpfulness (default: 0) */
|
|
26
|
+
helpfulNo?: number;
|
|
27
|
+
/** Callback when user votes on helpfulness (returns null when un-voting) */
|
|
28
|
+
onHelpfulVote?: (reviewId: string | number, vote: 'yes' | 'no' | null) => void;
|
|
29
|
+
/** Whether to show the helpful voting section (default: true) */
|
|
30
|
+
showHelpfulSection?: boolean;
|
|
31
|
+
/** Size of the avatar in pixels (default: 48) */
|
|
32
|
+
avatarSize?: number;
|
|
33
|
+
/** Maximum rating value (default: 5) */
|
|
34
|
+
maxRating?: number;
|
|
35
|
+
/** Whether the rating is read-only (default: true) */
|
|
36
|
+
readOnly?: boolean;
|
|
37
|
+
/** Whether to show the divider at the bottom of the review (default: true) */
|
|
38
|
+
showDivider?: boolean;
|
|
39
|
+
/** Whether to show skeleton loading state instead of content (default: false) */
|
|
40
|
+
loading?: boolean;
|
|
41
|
+
/** Additional styles for the container */
|
|
42
|
+
style?: StyleProp<ViewStyle>;
|
|
43
|
+
/** Callback when an image is tapped */
|
|
44
|
+
onImagePress?: (image: string | ImageSourcePropType, index: number) => void;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* A comprehensive review component that displays user reviews with avatar, name, rating, content, and helpful voting system.
|
|
48
|
+
* Built for React Native with consistent theming.
|
|
49
|
+
*/
|
|
50
|
+
export declare const SReview: {
|
|
51
|
+
({ id, userName, userAvatar, rating, reviewContent, datetime, images, userHelpfulVote, helpfulYes, helpfulNo, onHelpfulVote, showHelpfulSection, avatarSize, maxRating, readOnly, showDivider, loading, style, onImagePress, }: SReviewProps): React.JSX.Element;
|
|
52
|
+
displayName: string;
|
|
53
|
+
};
|
|
54
|
+
export default SReview;
|
|
55
|
+
//# sourceMappingURL=s-review.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"s-review.d.ts","sourceRoot":"","sources":["../../src/s-review/s-review.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA8C,MAAM,OAAO,CAAC;AACnE,OAAO,EAKL,KAAK,mBAAmB,EACxB,KAAK,SAAS,EACd,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AAUtB;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,iEAAiE;IACjE,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACrB,2BAA2B;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,4FAA4F;IAC5F,UAAU,CAAC,EAAE,MAAM,GAAG,mBAAmB,CAAC;IAC1C,mBAAmB;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,8BAA8B;IAC9B,aAAa,EAAE,MAAM,CAAC;IACtB,4EAA4E;IAC5E,QAAQ,EAAE,IAAI,GAAG,MAAM,CAAC;IACxB,qEAAqE;IACrE,MAAM,CAAC,EAAE,CAAC,MAAM,GAAG,mBAAmB,CAAC,EAAE,CAAC;IAC1C,+DAA+D;IAC/D,eAAe,CAAC,EAAE,KAAK,GAAG,IAAI,GAAG,IAAI,CAAC;IACtC,yDAAyD;IACzD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,wDAAwD;IACxD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4EAA4E;IAC5E,aAAa,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE,IAAI,EAAE,KAAK,GAAG,IAAI,GAAG,IAAI,KAAK,IAAI,CAAC;IAC/E,iEAAiE;IACjE,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,iDAAiD;IACjD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,wCAAwC;IACxC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,sDAAsD;IACtD,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,8EAA8E;IAC9E,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,iFAAiF;IACjF,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,0CAA0C;IAC1C,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,uCAAuC;IACvC,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,mBAAmB,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7E;AAED;;;GAGG;AACH,eAAO,MAAM,OAAO;oOAoBjB,YAAY,GAAG,KAAK,CAAC,GAAG,CAAC,OAAO;;CA+QlC,CAAC;AAIF,eAAe,OAAO,CAAC"}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useId, useMemo, useState } from 'react';
|
|
3
|
+
import { Image, Pressable, ScrollView, View, } from 'react-native';
|
|
4
|
+
import { brand, getRelativeTime, gray, lightBrand, lightGray, red } from '@solostylist/core';
|
|
5
|
+
import { SAvatar } from '../s-avatar';
|
|
6
|
+
import { SDivider } from '../s-divider';
|
|
7
|
+
import { SIconButton } from '../s-icon-button';
|
|
8
|
+
import { SRating } from '../s-rating';
|
|
9
|
+
import { SSkeleton } from '../s-skeleton';
|
|
10
|
+
import { SText } from '../s-text';
|
|
11
|
+
import { useSTheme } from '../theme';
|
|
12
|
+
/**
|
|
13
|
+
* A comprehensive review component that displays user reviews with avatar, name, rating, content, and helpful voting system.
|
|
14
|
+
* Built for React Native with consistent theming.
|
|
15
|
+
*/
|
|
16
|
+
export const SReview = ({ id, userName, userAvatar, rating, reviewContent, datetime, images, userHelpfulVote = null, helpfulYes = 0, helpfulNo = 0, onHelpfulVote, showHelpfulSection = true, avatarSize = 48, maxRating = 5, readOnly = true, showDivider = true, loading = false, style, onImagePress, }) => {
|
|
17
|
+
const { theme } = useSTheme();
|
|
18
|
+
const generatedId = useId();
|
|
19
|
+
const reviewId = id ?? generatedId;
|
|
20
|
+
const [localHelpfulYes, setLocalHelpfulYes] = useState(helpfulYes);
|
|
21
|
+
const [localHelpfulNo, setLocalHelpfulNo] = useState(helpfulNo);
|
|
22
|
+
const [userVote, setUserVote] = useState(userHelpfulVote);
|
|
23
|
+
const relativeTime = getRelativeTime(datetime);
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
setUserVote(userHelpfulVote);
|
|
26
|
+
}, [userHelpfulVote]);
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
setLocalHelpfulYes(helpfulYes);
|
|
29
|
+
}, [helpfulYes]);
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
setLocalHelpfulNo(helpfulNo);
|
|
32
|
+
}, [helpfulNo]);
|
|
33
|
+
const handleHelpfulVote = (vote) => {
|
|
34
|
+
let newVote = vote;
|
|
35
|
+
if (userVote === vote) {
|
|
36
|
+
// User is un-voting
|
|
37
|
+
if (vote === 'yes') {
|
|
38
|
+
setLocalHelpfulYes((prev) => Math.max(0, prev - 1));
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
setLocalHelpfulNo((prev) => Math.max(0, prev - 1));
|
|
42
|
+
}
|
|
43
|
+
setUserVote(null);
|
|
44
|
+
newVote = null;
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
// User is voting or changing vote
|
|
48
|
+
if (userVote === 'yes') {
|
|
49
|
+
setLocalHelpfulYes((prev) => Math.max(0, prev - 1));
|
|
50
|
+
}
|
|
51
|
+
else if (userVote === 'no') {
|
|
52
|
+
setLocalHelpfulNo((prev) => Math.max(0, prev - 1));
|
|
53
|
+
}
|
|
54
|
+
if (vote === 'yes') {
|
|
55
|
+
setLocalHelpfulYes((prev) => prev + 1);
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
setLocalHelpfulNo((prev) => prev + 1);
|
|
59
|
+
}
|
|
60
|
+
setUserVote(vote);
|
|
61
|
+
newVote = vote;
|
|
62
|
+
}
|
|
63
|
+
if (onHelpfulVote) {
|
|
64
|
+
onHelpfulVote(reviewId, newVote);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
// Calculate colors based on theme mode
|
|
68
|
+
const colors = useMemo(() => {
|
|
69
|
+
const isDark = theme.dark;
|
|
70
|
+
// Get color palettes
|
|
71
|
+
const primaryPalette = isDark ? brand : lightBrand;
|
|
72
|
+
const errorPalette = isDark ? red : red;
|
|
73
|
+
const grayPalette = isDark ? gray : lightGray;
|
|
74
|
+
return {
|
|
75
|
+
thumbUpActive: primaryPalette[500],
|
|
76
|
+
thumbDownActive: errorPalette[500],
|
|
77
|
+
thumbInactive: grayPalette[500],
|
|
78
|
+
textPrimary: theme.colors.text.primary,
|
|
79
|
+
textSecondary: grayPalette[600],
|
|
80
|
+
captionText: grayPalette[600],
|
|
81
|
+
};
|
|
82
|
+
}, [theme.dark, theme.colors.text.primary]);
|
|
83
|
+
// Skeleton loading state
|
|
84
|
+
if (loading) {
|
|
85
|
+
return (_jsxs(View, { style: [
|
|
86
|
+
{
|
|
87
|
+
paddingVertical: theme.spacing.md,
|
|
88
|
+
},
|
|
89
|
+
style,
|
|
90
|
+
], children: [_jsxs(View, { style: {
|
|
91
|
+
flexDirection: 'row',
|
|
92
|
+
gap: theme.spacing.md,
|
|
93
|
+
}, children: [_jsx(SSkeleton, { variant: "circular", width: avatarSize, height: avatarSize }), _jsxs(View, { style: {
|
|
94
|
+
flex: 1,
|
|
95
|
+
}, children: [_jsxs(View, { style: {
|
|
96
|
+
flexDirection: 'row',
|
|
97
|
+
alignItems: 'center',
|
|
98
|
+
gap: theme.spacing.md,
|
|
99
|
+
marginBottom: theme.spacing.xs,
|
|
100
|
+
}, children: [_jsx(SSkeleton, { variant: "text", width: 120, height: 14 }), _jsx(SSkeleton, { variant: "text", width: 80, height: 12 })] }), _jsx(SSkeleton, { variant: "text", width: 100, height: 20, style: { marginBottom: theme.spacing.xs } }), _jsx(SSkeleton, { variant: "text", width: "100%", height: 16, style: { marginBottom: 4 } }), _jsx(SSkeleton, { variant: "text", width: "90%", height: 16, style: { marginBottom: theme.spacing.md } }), showHelpfulSection && (_jsxs(View, { style: {
|
|
101
|
+
flexDirection: 'row',
|
|
102
|
+
alignItems: 'center',
|
|
103
|
+
gap: theme.spacing.xs,
|
|
104
|
+
marginTop: theme.spacing.sm,
|
|
105
|
+
}, children: [_jsx(SSkeleton, { variant: "text", width: 140, height: 14 }), _jsx(SSkeleton, { variant: "circular", width: 32, height: 32, style: { borderRadius: theme.borderRadius.sm } }), _jsx(SSkeleton, { variant: "text", width: 20, height: 14 }), _jsx(SSkeleton, { variant: "circular", width: 32, height: 32, style: { borderRadius: theme.borderRadius.sm } }), _jsx(SSkeleton, { variant: "text", width: 20, height: 14 })] }))] })] }), showDivider && _jsx(SDivider, {})] }));
|
|
106
|
+
}
|
|
107
|
+
return (_jsxs(View, { style: [
|
|
108
|
+
{
|
|
109
|
+
paddingVertical: theme.spacing.md,
|
|
110
|
+
},
|
|
111
|
+
style,
|
|
112
|
+
], children: [_jsxs(View, { style: {
|
|
113
|
+
flexDirection: 'row',
|
|
114
|
+
gap: theme.spacing.md,
|
|
115
|
+
}, children: [_jsx(View, { style: {
|
|
116
|
+
paddingTop: 2,
|
|
117
|
+
}, children: _jsx(SAvatar, { name: userName, avatar: userAvatar, size: avatarSize }) }), _jsxs(View, { style: {
|
|
118
|
+
flex: 1,
|
|
119
|
+
}, children: [_jsxs(View, { style: {
|
|
120
|
+
flexDirection: 'row',
|
|
121
|
+
alignItems: 'center',
|
|
122
|
+
gap: theme.spacing.md,
|
|
123
|
+
marginBottom: theme.spacing.xs,
|
|
124
|
+
}, children: [_jsx(SText, { children: userName }), _jsx(SText, { variant: "caption", children: relativeTime })] }), _jsx(View, { style: {
|
|
125
|
+
marginBottom: theme.spacing.xs,
|
|
126
|
+
}, children: _jsx(SRating, { value: rating, max: maxRating, readOnly: readOnly, size: "small" }) }), _jsx(SText, { style: {
|
|
127
|
+
marginBottom: theme.spacing.md,
|
|
128
|
+
}, children: reviewContent }), images && images.length > 0 && (_jsx(ScrollView, { horizontal: true, showsHorizontalScrollIndicator: false, style: {
|
|
129
|
+
flexDirection: 'row',
|
|
130
|
+
gap: theme.spacing.xs,
|
|
131
|
+
marginBottom: theme.spacing.md,
|
|
132
|
+
}, children: images.map((image, index) => (_jsx(Pressable, { style: {
|
|
133
|
+
width: 80,
|
|
134
|
+
height: 80,
|
|
135
|
+
borderRadius: theme.borderRadius.sm,
|
|
136
|
+
overflow: 'hidden',
|
|
137
|
+
}, onPress: () => onImagePress?.(image, index), children: _jsx(Image, { source: typeof image === 'string' ? { uri: image } : image, style: {
|
|
138
|
+
width: '100%',
|
|
139
|
+
height: '100%',
|
|
140
|
+
resizeMode: 'cover',
|
|
141
|
+
} }) }, index))) })), showHelpfulSection && (_jsxs(View, { style: {
|
|
142
|
+
flexDirection: 'row',
|
|
143
|
+
alignItems: 'center',
|
|
144
|
+
gap: theme.spacing.xs,
|
|
145
|
+
}, children: [_jsx(SText, { variant: "caption", style: { marginRight: theme.spacing.sm }, children: "Was this review helpful?" }), _jsx(SIconButton, { icon: userVote === 'yes' ? 'thumb-up' : 'thumb-up-outline', iconColor: userVote === 'yes' ? colors.thumbUpActive : colors.thumbInactive, onPress: () => handleHelpfulVote('yes') }), _jsx(SText, { variant: "caption", style: { marginLeft: 2 }, children: localHelpfulYes }), _jsx(SIconButton, { icon: userVote === 'no' ? 'thumb-down' : 'thumb-down-outline', iconColor: userVote === 'no' ? colors.thumbDownActive : colors.thumbInactive, onPress: () => handleHelpfulVote('no'), style: { marginLeft: theme.spacing.xs } }), _jsx(SText, { variant: "caption", style: { marginLeft: 2 }, children: localHelpfulNo })] }))] })] }), showDivider && _jsx(SDivider, {})] }));
|
|
146
|
+
};
|
|
147
|
+
SReview.displayName = 'SReview';
|
|
148
|
+
export default SReview;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/s-scroll-to-top/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,KAAK,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACzE,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { type StyleProp, type ViewStyle } from 'react-native';
|
|
3
|
+
/**
|
|
4
|
+
* Props interface for SScrollToTop component
|
|
5
|
+
*/
|
|
6
|
+
export interface SScrollToTopProps {
|
|
7
|
+
/** Handler called when the scroll-to-top button is clicked */
|
|
8
|
+
onPress?: () => void;
|
|
9
|
+
/** Whether the button should be visible (controlled externally via scroll position) */
|
|
10
|
+
visible: boolean;
|
|
11
|
+
/** Custom icon to display instead of the default arrow */
|
|
12
|
+
icon?: React.ReactNode;
|
|
13
|
+
/** Position from the bottom edge (default: 20) */
|
|
14
|
+
bottom?: number;
|
|
15
|
+
/** Position from the right edge (default: 20) */
|
|
16
|
+
right?: number;
|
|
17
|
+
/** Position from the left edge (omit to use right positioning) */
|
|
18
|
+
left?: number;
|
|
19
|
+
/** Button size (width and height, default: 40) */
|
|
20
|
+
size?: number;
|
|
21
|
+
/** Background color (defaults to theme background.paper) */
|
|
22
|
+
backgroundColor?: string;
|
|
23
|
+
/** Icon color (defaults to theme text color) */
|
|
24
|
+
iconColor?: string;
|
|
25
|
+
/** Custom style for the button container */
|
|
26
|
+
style?: StyleProp<ViewStyle>;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* A floating button that appears when the user scrolls down and allows them to quickly return to the top.
|
|
30
|
+
*
|
|
31
|
+
* Note: For React Native, visibility must be controlled externally via the `visible` prop.
|
|
32
|
+
* Track scroll position using ScrollView's `onScroll` event and update `visible` accordingly.
|
|
33
|
+
*/
|
|
34
|
+
export declare const SScrollToTop: React.MemoExoticComponent<({ onPress, visible, icon, bottom, right, left, size, backgroundColor, iconColor, style, }: SScrollToTopProps) => React.JSX.Element>;
|
|
35
|
+
export default SScrollToTop;
|
|
36
|
+
//# sourceMappingURL=s-scroll-to-top.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"s-scroll-to-top.d.ts","sourceRoot":"","sources":["../../src/s-scroll-to-top/s-scroll-to-top.tsx"],"names":[],"mappings":"AACA,OAAO,KAAoB,MAAM,OAAO,CAAC;AACzC,OAAO,EAAuB,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAGnF;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,8DAA8D;IAC9D,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,uFAAuF;IACvF,OAAO,EAAE,OAAO,CAAC;IACjB,0DAA0D;IAC1D,IAAI,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACvB,kDAAkD;IAClD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,iDAAiD;IACjD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kEAAkE;IAClE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,kDAAkD;IAClD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,4DAA4D;IAC5D,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,gDAAgD;IAChD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4CAA4C;IAC5C,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAC9B;AAED;;;;;GAKG;AACH,eAAO,MAAM,YAAY,wHAYpB,iBAAiB,KAAG,KAAK,CAAC,GAAG,CAAC,OAAO,CAmEzC,CAAC;AAIF,eAAe,YAAY,CAAC"}
|