@papernote/ui 1.3.1 → 1.6.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 (108) hide show
  1. package/dist/components/ActionBar.d.ts +112 -0
  2. package/dist/components/ActionBar.d.ts.map +1 -0
  3. package/dist/components/BottomNavigation.d.ts +98 -0
  4. package/dist/components/BottomNavigation.d.ts.map +1 -0
  5. package/dist/components/Checkbox.d.ts +2 -0
  6. package/dist/components/Checkbox.d.ts.map +1 -1
  7. package/dist/components/CheckboxList.d.ts +81 -0
  8. package/dist/components/CheckboxList.d.ts.map +1 -0
  9. package/dist/components/Chip.d.ts +92 -1
  10. package/dist/components/Chip.d.ts.map +1 -1
  11. package/dist/components/ConfirmDialog.d.ts +43 -1
  12. package/dist/components/ConfirmDialog.d.ts.map +1 -1
  13. package/dist/components/DataTable.d.ts +10 -1
  14. package/dist/components/DataTable.d.ts.map +1 -1
  15. package/dist/components/DataTableCardView.d.ts +99 -0
  16. package/dist/components/DataTableCardView.d.ts.map +1 -0
  17. package/dist/components/ExpandablePanel.d.ts +142 -0
  18. package/dist/components/ExpandablePanel.d.ts.map +1 -0
  19. package/dist/components/FloatingActionButton.d.ts +98 -0
  20. package/dist/components/FloatingActionButton.d.ts.map +1 -0
  21. package/dist/components/Input.d.ts +45 -1
  22. package/dist/components/Input.d.ts.map +1 -1
  23. package/dist/components/MobileHeader.d.ts +98 -0
  24. package/dist/components/MobileHeader.d.ts.map +1 -0
  25. package/dist/components/MobileLayout.d.ts +121 -0
  26. package/dist/components/MobileLayout.d.ts.map +1 -0
  27. package/dist/components/Modal.d.ts +78 -1
  28. package/dist/components/Modal.d.ts.map +1 -1
  29. package/dist/components/PageHeader.d.ts +86 -0
  30. package/dist/components/PageHeader.d.ts.map +1 -0
  31. package/dist/components/PullToRefresh.d.ts +87 -0
  32. package/dist/components/PullToRefresh.d.ts.map +1 -0
  33. package/dist/components/QueryTransparency.d.ts +1 -1
  34. package/dist/components/QueryTransparency.d.ts.map +1 -1
  35. package/dist/components/SearchableList.d.ts +83 -0
  36. package/dist/components/SearchableList.d.ts.map +1 -0
  37. package/dist/components/Select.d.ts +16 -2
  38. package/dist/components/Select.d.ts.map +1 -1
  39. package/dist/components/Sidebar.d.ts +40 -1
  40. package/dist/components/Sidebar.d.ts.map +1 -1
  41. package/dist/components/SwipeActions.d.ts +93 -0
  42. package/dist/components/SwipeActions.d.ts.map +1 -0
  43. package/dist/components/Switch.d.ts +1 -0
  44. package/dist/components/Switch.d.ts.map +1 -1
  45. package/dist/components/Textarea.d.ts +13 -0
  46. package/dist/components/Textarea.d.ts.map +1 -1
  47. package/dist/components/index.d.ts +31 -3
  48. package/dist/components/index.d.ts.map +1 -1
  49. package/dist/context/MobileContext.d.ts +168 -0
  50. package/dist/context/MobileContext.d.ts.map +1 -0
  51. package/dist/hooks/useResponsive.d.ts +158 -0
  52. package/dist/hooks/useResponsive.d.ts.map +1 -0
  53. package/dist/index.d.ts +1871 -51
  54. package/dist/index.esm.js +3025 -196
  55. package/dist/index.esm.js.map +1 -1
  56. package/dist/index.js +3063 -194
  57. package/dist/index.js.map +1 -1
  58. package/dist/styles.css +434 -1
  59. package/dist/types/index.d.ts +2 -0
  60. package/dist/types/index.d.ts.map +1 -1
  61. package/package.json +1 -1
  62. package/src/components/ActionBar.stories.tsx +246 -0
  63. package/src/components/ActionBar.tsx +242 -0
  64. package/src/components/BottomNavigation.stories.tsx +142 -0
  65. package/src/components/BottomNavigation.tsx +225 -0
  66. package/src/components/Checkbox.stories.tsx +162 -0
  67. package/src/components/Checkbox.tsx +22 -6
  68. package/src/components/CheckboxList.stories.tsx +311 -0
  69. package/src/components/CheckboxList.tsx +433 -0
  70. package/src/components/Chip.stories.tsx +389 -0
  71. package/src/components/Chip.tsx +182 -3
  72. package/src/components/ConfirmDialog.tsx +56 -4
  73. package/src/components/DataTable.tsx +60 -1
  74. package/src/components/DataTableCardView.stories.tsx +307 -0
  75. package/src/components/DataTableCardView.tsx +419 -0
  76. package/src/components/ExpandablePanel.stories.tsx +620 -0
  77. package/src/components/ExpandablePanel.tsx +383 -0
  78. package/src/components/FloatingActionButton.stories.tsx +197 -0
  79. package/src/components/FloatingActionButton.tsx +301 -0
  80. package/src/components/Grid.stories.tsx +16 -16
  81. package/src/components/Input.stories.tsx +214 -0
  82. package/src/components/Input.tsx +81 -4
  83. package/src/components/MobileHeader.stories.tsx +205 -0
  84. package/src/components/MobileHeader.tsx +233 -0
  85. package/src/components/MobileLayout.stories.tsx +338 -0
  86. package/src/components/MobileLayout.tsx +313 -0
  87. package/src/components/Modal.stories.tsx +388 -0
  88. package/src/components/Modal.tsx +122 -4
  89. package/src/components/PageHeader.stories.tsx +198 -0
  90. package/src/components/PageHeader.tsx +217 -0
  91. package/src/components/PullToRefresh.stories.tsx +321 -0
  92. package/src/components/PullToRefresh.tsx +294 -0
  93. package/src/components/QueryTransparency.tsx +1 -1
  94. package/src/components/SearchableList.stories.tsx +437 -0
  95. package/src/components/SearchableList.tsx +326 -0
  96. package/src/components/Select.stories.tsx +190 -0
  97. package/src/components/Select.tsx +353 -137
  98. package/src/components/Sidebar.tsx +193 -10
  99. package/src/components/SwipeActions.stories.tsx +327 -0
  100. package/src/components/SwipeActions.tsx +387 -0
  101. package/src/components/Switch.stories.tsx +158 -0
  102. package/src/components/Switch.tsx +12 -3
  103. package/src/components/Textarea.tsx +31 -1
  104. package/src/components/index.ts +69 -3
  105. package/src/context/MobileContext.tsx +296 -0
  106. package/src/hooks/useResponsive.ts +360 -0
  107. package/src/types/index.ts +4 -0
  108. package/tailwind.config.js +56 -1
@@ -0,0 +1,321 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { useState } from 'react';
3
+ import { RefreshCw } from 'lucide-react';
4
+ import PullToRefresh from './PullToRefresh';
5
+
6
+ const meta: Meta<typeof PullToRefresh> = {
7
+ title: 'Mobile/PullToRefresh',
8
+ component: PullToRefresh,
9
+ parameters: {
10
+ layout: 'fullscreen',
11
+ viewport: {
12
+ defaultViewport: 'mobile1',
13
+ },
14
+ docs: {
15
+ description: {
16
+ component: 'Native-feeling pull-to-refresh gesture handler for mobile content. Only activates when scrolled to top.',
17
+ },
18
+ },
19
+ },
20
+ };
21
+
22
+ export default meta;
23
+ type Story = StoryObj<typeof PullToRefresh>;
24
+
25
+ // Helper component for interactive stories
26
+ const RefreshableList = ({
27
+ itemCount = 10,
28
+ pullThreshold = 80,
29
+ maxPull = 120,
30
+ disabled = false,
31
+ }: {
32
+ itemCount?: number;
33
+ pullThreshold?: number;
34
+ maxPull?: number;
35
+ disabled?: boolean;
36
+ }) => {
37
+ const [items, setItems] = useState(() =>
38
+ Array.from({ length: itemCount }, (_, i) => ({
39
+ id: i + 1,
40
+ title: `Item ${i + 1}`,
41
+ timestamp: new Date().toLocaleTimeString(),
42
+ }))
43
+ );
44
+ const [refreshCount, setRefreshCount] = useState(0);
45
+
46
+ const handleRefresh = async () => {
47
+ // Simulate API call
48
+ await new Promise(resolve => setTimeout(resolve, 1500));
49
+
50
+ setRefreshCount(prev => prev + 1);
51
+ setItems(prev => [
52
+ {
53
+ id: Date.now(),
54
+ title: `New Item (Refresh #${refreshCount + 1})`,
55
+ timestamp: new Date().toLocaleTimeString(),
56
+ },
57
+ ...prev,
58
+ ]);
59
+ };
60
+
61
+ return (
62
+ <PullToRefresh
63
+ onRefresh={handleRefresh}
64
+ pullThreshold={pullThreshold}
65
+ maxPull={maxPull}
66
+ disabled={disabled}
67
+ className="h-screen"
68
+ >
69
+ <div style={{ padding: '16px', background: '#f5f5f4', minHeight: '100vh' }}>
70
+ <div style={{
71
+ padding: '12px 16px',
72
+ background: '#fef3c7',
73
+ borderRadius: '8px',
74
+ marginBottom: '16px',
75
+ fontSize: '14px',
76
+ }}>
77
+ <strong>Pull down to refresh</strong>
78
+ <p style={{ color: '#666', marginTop: '4px' }}>
79
+ Refreshed {refreshCount} times. Items: {items.length}
80
+ </p>
81
+ </div>
82
+
83
+ {items.map((item) => (
84
+ <div
85
+ key={item.id}
86
+ style={{
87
+ padding: '16px',
88
+ margin: '8px 0',
89
+ background: 'white',
90
+ borderRadius: '8px',
91
+ boxShadow: '0 1px 3px rgba(0,0,0,0.1)',
92
+ }}
93
+ >
94
+ <div style={{ fontWeight: '500' }}>{item.title}</div>
95
+ <div style={{ fontSize: '12px', color: '#666', marginTop: '4px' }}>
96
+ Added at {item.timestamp}
97
+ </div>
98
+ </div>
99
+ ))}
100
+ </div>
101
+ </PullToRefresh>
102
+ );
103
+ };
104
+
105
+ export const Default: Story = {
106
+ render: () => <RefreshableList />,
107
+ parameters: {
108
+ docs: {
109
+ description: {
110
+ story: 'Pull down from the top to trigger a refresh. The spinner appears after pulling past the threshold.',
111
+ },
112
+ },
113
+ },
114
+ };
115
+
116
+ export const LowThreshold: Story = {
117
+ render: () => <RefreshableList pullThreshold={50} maxPull={80} />,
118
+ parameters: {
119
+ docs: {
120
+ description: {
121
+ story: 'Lower threshold (50px) makes it easier to trigger refresh.',
122
+ },
123
+ },
124
+ },
125
+ };
126
+
127
+ export const HighThreshold: Story = {
128
+ render: () => <RefreshableList pullThreshold={120} maxPull={180} />,
129
+ parameters: {
130
+ docs: {
131
+ description: {
132
+ story: 'Higher threshold (120px) requires more pull distance to trigger refresh.',
133
+ },
134
+ },
135
+ },
136
+ };
137
+
138
+ export const Disabled: Story = {
139
+ render: () => <RefreshableList disabled />,
140
+ parameters: {
141
+ docs: {
142
+ description: {
143
+ story: 'Pull-to-refresh can be disabled when not needed.',
144
+ },
145
+ },
146
+ },
147
+ };
148
+
149
+ export const CustomIndicator: Story = {
150
+ render: () => {
151
+ const [items, setItems] = useState(
152
+ Array.from({ length: 5 }, (_, i) => `Item ${i + 1}`)
153
+ );
154
+
155
+ const handleRefresh = async () => {
156
+ await new Promise(resolve => setTimeout(resolve, 1000));
157
+ setItems(prev => [`New Item ${Date.now()}`, ...prev]);
158
+ };
159
+
160
+ return (
161
+ <PullToRefresh
162
+ onRefresh={handleRefresh}
163
+ loadingIndicator={
164
+ <RefreshCw className="h-6 w-6 text-green-600 animate-spin" />
165
+ }
166
+ pullIndicator={
167
+ <RefreshCw className="h-6 w-6 text-gray-400" />
168
+ }
169
+ className="h-screen"
170
+ >
171
+ <div style={{ padding: '16px', background: '#f5f5f4', minHeight: '100vh' }}>
172
+ <div style={{
173
+ padding: '12px 16px',
174
+ background: '#dcfce7',
175
+ borderRadius: '8px',
176
+ marginBottom: '16px',
177
+ }}>
178
+ Custom refresh indicator (green spinner)
179
+ </div>
180
+ {items.map((item, i) => (
181
+ <div
182
+ key={i}
183
+ style={{
184
+ padding: '16px',
185
+ margin: '8px 0',
186
+ background: 'white',
187
+ borderRadius: '8px'
188
+ }}
189
+ >
190
+ {item}
191
+ </div>
192
+ ))}
193
+ </div>
194
+ </PullToRefresh>
195
+ );
196
+ },
197
+ };
198
+
199
+ export const WithLongContent: Story = {
200
+ render: () => <RefreshableList itemCount={30} />,
201
+ parameters: {
202
+ docs: {
203
+ description: {
204
+ story: 'With long scrollable content. Pull-to-refresh only activates when scrolled to the very top.',
205
+ },
206
+ },
207
+ },
208
+ };
209
+
210
+ export const EmptyState: Story = {
211
+ render: () => {
212
+ const [items, setItems] = useState<string[]>([]);
213
+ const [loading, setLoading] = useState(false);
214
+
215
+ const handleRefresh = async () => {
216
+ setLoading(true);
217
+ await new Promise(resolve => setTimeout(resolve, 1500));
218
+ setItems(['Fetched Item 1', 'Fetched Item 2', 'Fetched Item 3']);
219
+ setLoading(false);
220
+ };
221
+
222
+ return (
223
+ <PullToRefresh onRefresh={handleRefresh} className="h-screen">
224
+ <div style={{
225
+ padding: '16px',
226
+ background: '#f5f5f4',
227
+ minHeight: '100vh',
228
+ display: 'flex',
229
+ flexDirection: 'column',
230
+ }}>
231
+ {items.length === 0 ? (
232
+ <div style={{
233
+ flex: 1,
234
+ display: 'flex',
235
+ flexDirection: 'column',
236
+ alignItems: 'center',
237
+ justifyContent: 'center',
238
+ color: '#666',
239
+ }}>
240
+ <RefreshCw className="h-12 w-12 mb-4 text-gray-300" />
241
+ <p style={{ fontWeight: '500' }}>No items yet</p>
242
+ <p style={{ fontSize: '14px' }}>Pull down to load items</p>
243
+ </div>
244
+ ) : (
245
+ items.map((item, i) => (
246
+ <div
247
+ key={i}
248
+ style={{
249
+ padding: '16px',
250
+ margin: '8px 0',
251
+ background: 'white',
252
+ borderRadius: '8px'
253
+ }}
254
+ >
255
+ {item}
256
+ </div>
257
+ ))
258
+ )}
259
+ </div>
260
+ </PullToRefresh>
261
+ );
262
+ },
263
+ parameters: {
264
+ docs: {
265
+ description: {
266
+ story: 'Pull-to-refresh works well with empty states to load initial data.',
267
+ },
268
+ },
269
+ },
270
+ };
271
+
272
+ export const MobileInstructions: Story = {
273
+ render: () => (
274
+ <div style={{ padding: '24px', background: '#f5f5f4', minHeight: '100vh' }}>
275
+ <h2 style={{ fontSize: '20px', fontWeight: 'bold', marginBottom: '16px' }}>
276
+ Pull to Refresh Component
277
+ </h2>
278
+
279
+ <div style={{ background: 'white', padding: '16px', borderRadius: '8px', marginBottom: '16px' }}>
280
+ <h3 style={{ fontWeight: '600', marginBottom: '8px' }}>Usage</h3>
281
+ <pre style={{
282
+ background: '#f1f5f9',
283
+ padding: '12px',
284
+ borderRadius: '4px',
285
+ fontSize: '12px',
286
+ overflow: 'auto',
287
+ }}>
288
+ {`<PullToRefresh
289
+ onRefresh={async () => {
290
+ await fetchLatestData();
291
+ }}
292
+ pullThreshold={80}
293
+ maxPull={120}
294
+ >
295
+ {content}
296
+ </PullToRefresh>`}
297
+ </pre>
298
+ </div>
299
+
300
+ <div style={{ background: 'white', padding: '16px', borderRadius: '8px', marginBottom: '16px' }}>
301
+ <h3 style={{ fontWeight: '600', marginBottom: '8px' }}>Props</h3>
302
+ <ul style={{ fontSize: '14px', lineHeight: '1.6' }}>
303
+ <li><strong>onRefresh</strong>: Async function called on refresh</li>
304
+ <li><strong>pullThreshold</strong>: Distance to trigger (default: 80px)</li>
305
+ <li><strong>maxPull</strong>: Maximum pull distance (default: 120px)</li>
306
+ <li><strong>disabled</strong>: Disable pull-to-refresh</li>
307
+ <li><strong>loadingIndicator</strong>: Custom loading spinner</li>
308
+ <li><strong>pullIndicator</strong>: Custom pull indicator</li>
309
+ </ul>
310
+ </div>
311
+
312
+ <div style={{ background: '#fef3c7', padding: '16px', borderRadius: '8px' }}>
313
+ <h3 style={{ fontWeight: '600', marginBottom: '8px' }}>Testing</h3>
314
+ <p style={{ fontSize: '14px' }}>
315
+ To test on desktop, use Chrome DevTools mobile emulation with touch simulation enabled.
316
+ Click and drag down from the top of the content area.
317
+ </p>
318
+ </div>
319
+ </div>
320
+ ),
321
+ };
@@ -0,0 +1,294 @@
1
+ import React, { useState, useRef, useCallback, useEffect } from 'react';
2
+ import { Loader2, ArrowDown } from 'lucide-react';
3
+
4
+ /**
5
+ * PullToRefresh component props
6
+ */
7
+ export interface PullToRefreshProps {
8
+ /** Content to wrap */
9
+ children: React.ReactNode;
10
+ /** Async refresh handler - should return a Promise */
11
+ onRefresh: () => Promise<void>;
12
+ /** Disable pull-to-refresh */
13
+ disabled?: boolean;
14
+ /** Pull distance required to trigger refresh (default: 80) */
15
+ pullThreshold?: number;
16
+ /** Maximum pull distance (default: 120) */
17
+ maxPull?: number;
18
+ /** Custom loading indicator */
19
+ loadingIndicator?: React.ReactNode;
20
+ /** Custom pull indicator */
21
+ pullIndicator?: React.ReactNode;
22
+ /** Additional class names for container */
23
+ className?: string;
24
+ }
25
+
26
+ type RefreshState = 'idle' | 'pulling' | 'ready' | 'refreshing';
27
+
28
+ /**
29
+ * PullToRefresh - Mobile pull-to-refresh gesture handler
30
+ *
31
+ * Wraps content and provides native-feeling pull-to-refresh functionality.
32
+ * Only activates when scrolled to top of content.
33
+ *
34
+ * @example Basic usage
35
+ * ```tsx
36
+ * <PullToRefresh onRefresh={async () => {
37
+ * await fetchLatestData();
38
+ * }}>
39
+ * <div className="min-h-screen">
40
+ * {content}
41
+ * </div>
42
+ * </PullToRefresh>
43
+ * ```
44
+ *
45
+ * @example With custom threshold
46
+ * ```tsx
47
+ * <PullToRefresh
48
+ * onRefresh={handleRefresh}
49
+ * pullThreshold={100}
50
+ * maxPull={150}
51
+ * >
52
+ * {content}
53
+ * </PullToRefresh>
54
+ * ```
55
+ */
56
+ export default function PullToRefresh({
57
+ children,
58
+ onRefresh,
59
+ disabled = false,
60
+ pullThreshold = 80,
61
+ maxPull = 120,
62
+ loadingIndicator,
63
+ pullIndicator,
64
+ className = '',
65
+ }: PullToRefreshProps) {
66
+ const [state, setState] = useState<RefreshState>('idle');
67
+ const [pullDistance, setPullDistance] = useState(0);
68
+ const containerRef = useRef<HTMLDivElement>(null);
69
+ const startY = useRef(0);
70
+ const currentY = useRef(0);
71
+
72
+ // Check if at top of scroll container
73
+ const isAtTop = useCallback(() => {
74
+ const container = containerRef.current;
75
+ if (!container) return false;
76
+ return container.scrollTop <= 0;
77
+ }, []);
78
+
79
+ // Handle touch start
80
+ const handleTouchStart = useCallback((e: TouchEvent) => {
81
+ if (disabled || state === 'refreshing' || !isAtTop()) return;
82
+
83
+ startY.current = e.touches[0].clientY;
84
+ currentY.current = startY.current;
85
+ }, [disabled, state, isAtTop]);
86
+
87
+ // Handle touch move
88
+ const handleTouchMove = useCallback((e: TouchEvent) => {
89
+ if (disabled || state === 'refreshing') return;
90
+ if (startY.current === 0) return;
91
+
92
+ currentY.current = e.touches[0].clientY;
93
+ const diff = currentY.current - startY.current;
94
+
95
+ // Only allow pulling down when at top
96
+ if (diff > 0 && isAtTop()) {
97
+ // Apply resistance - pull slows down as distance increases
98
+ const resistance = 0.5;
99
+ const adjustedPull = Math.min(diff * resistance, maxPull);
100
+
101
+ setPullDistance(adjustedPull);
102
+ setState(adjustedPull >= pullThreshold ? 'ready' : 'pulling');
103
+
104
+ // Prevent default scroll when pulling
105
+ if (adjustedPull > 0) {
106
+ e.preventDefault();
107
+ }
108
+ }
109
+ }, [disabled, state, isAtTop, maxPull, pullThreshold]);
110
+
111
+ // Handle touch end
112
+ const handleTouchEnd = useCallback(async () => {
113
+ if (disabled || state === 'refreshing') return;
114
+
115
+ if (state === 'ready') {
116
+ setState('refreshing');
117
+ setPullDistance(pullThreshold); // Hold at threshold while refreshing
118
+
119
+ try {
120
+ await onRefresh();
121
+ } catch (error) {
122
+ console.error('Refresh failed:', error);
123
+ }
124
+
125
+ setState('idle');
126
+ }
127
+
128
+ setPullDistance(0);
129
+ startY.current = 0;
130
+ currentY.current = 0;
131
+ }, [disabled, state, pullThreshold, onRefresh]);
132
+
133
+ // Attach touch listeners
134
+ useEffect(() => {
135
+ const container = containerRef.current;
136
+ if (!container) return;
137
+
138
+ container.addEventListener('touchstart', handleTouchStart, { passive: true });
139
+ container.addEventListener('touchmove', handleTouchMove, { passive: false });
140
+ container.addEventListener('touchend', handleTouchEnd);
141
+
142
+ return () => {
143
+ container.removeEventListener('touchstart', handleTouchStart);
144
+ container.removeEventListener('touchmove', handleTouchMove);
145
+ container.removeEventListener('touchend', handleTouchEnd);
146
+ };
147
+ }, [handleTouchStart, handleTouchMove, handleTouchEnd]);
148
+
149
+ // Calculate indicator opacity and rotation
150
+ const progress = Math.min(pullDistance / pullThreshold, 1);
151
+ const rotation = progress * 180;
152
+
153
+ // Default loading indicator
154
+ const defaultLoadingIndicator = (
155
+ <Loader2 className="h-6 w-6 text-accent-600 animate-spin" />
156
+ );
157
+
158
+ // Default pull indicator
159
+ const defaultPullIndicator = (
160
+ <div
161
+ className={`
162
+ transition-transform duration-200
163
+ ${state === 'ready' ? 'text-accent-600' : 'text-ink-400'}
164
+ `}
165
+ style={{ transform: `rotate(${rotation}deg)` }}
166
+ >
167
+ <ArrowDown className="h-6 w-6" />
168
+ </div>
169
+ );
170
+
171
+ return (
172
+ <div
173
+ ref={containerRef}
174
+ className={`relative overflow-auto ${className}`}
175
+ style={{ touchAction: pullDistance > 0 ? 'none' : 'auto' }}
176
+ >
177
+ {/* Pull indicator */}
178
+ <div
179
+ className={`
180
+ absolute left-0 right-0 flex items-center justify-center
181
+ transition-all duration-200 overflow-hidden
182
+ ${state === 'idle' && pullDistance === 0 ? 'opacity-0' : 'opacity-100'}
183
+ `}
184
+ style={{
185
+ height: `${pullDistance}px`,
186
+ top: 0,
187
+ zIndex: 10,
188
+ }}
189
+ >
190
+ <div
191
+ className={`
192
+ w-10 h-10 rounded-full bg-white shadow-md
193
+ flex items-center justify-center
194
+ transition-transform duration-200
195
+ ${state === 'refreshing' ? 'scale-100' : progress < 0.3 ? 'scale-75' : 'scale-100'}
196
+ `}
197
+ >
198
+ {state === 'refreshing'
199
+ ? (loadingIndicator || defaultLoadingIndicator)
200
+ : (pullIndicator || defaultPullIndicator)
201
+ }
202
+ </div>
203
+ </div>
204
+
205
+ {/* Content wrapper */}
206
+ <div
207
+ className="transition-transform duration-200"
208
+ style={{
209
+ transform: `translateY(${pullDistance}px)`,
210
+ }}
211
+ >
212
+ {children}
213
+ </div>
214
+ </div>
215
+ );
216
+ }
217
+
218
+ /**
219
+ * usePullToRefresh - Hook for custom pull-to-refresh implementations
220
+ *
221
+ * @example
222
+ * ```tsx
223
+ * const { pullDistance, isRefreshing, bind } = usePullToRefresh({
224
+ * onRefresh: async () => {
225
+ * await fetchData();
226
+ * }
227
+ * });
228
+ *
229
+ * return (
230
+ * <div {...bind}>
231
+ * {isRefreshing && <Spinner />}
232
+ * {content}
233
+ * </div>
234
+ * );
235
+ * ```
236
+ */
237
+ export function usePullToRefresh({
238
+ onRefresh,
239
+ pullThreshold = 80,
240
+ maxPull = 120,
241
+ disabled = false,
242
+ }: {
243
+ onRefresh: () => Promise<void>;
244
+ pullThreshold?: number;
245
+ maxPull?: number;
246
+ disabled?: boolean;
247
+ }) {
248
+ const [pullDistance, setPullDistance] = useState(0);
249
+ const [isRefreshing, setIsRefreshing] = useState(false);
250
+ const startY = useRef(0);
251
+
252
+ const handleTouchStart = useCallback((e: React.TouchEvent) => {
253
+ if (disabled || isRefreshing) return;
254
+ startY.current = e.touches[0].clientY;
255
+ }, [disabled, isRefreshing]);
256
+
257
+ const handleTouchMove = useCallback((e: React.TouchEvent) => {
258
+ if (disabled || isRefreshing || startY.current === 0) return;
259
+
260
+ const diff = e.touches[0].clientY - startY.current;
261
+ if (diff > 0) {
262
+ const adjustedPull = Math.min(diff * 0.5, maxPull);
263
+ setPullDistance(adjustedPull);
264
+ }
265
+ }, [disabled, isRefreshing, maxPull]);
266
+
267
+ const handleTouchEnd = useCallback(async () => {
268
+ if (disabled || isRefreshing) return;
269
+
270
+ if (pullDistance >= pullThreshold) {
271
+ setIsRefreshing(true);
272
+ try {
273
+ await onRefresh();
274
+ } finally {
275
+ setIsRefreshing(false);
276
+ }
277
+ }
278
+
279
+ setPullDistance(0);
280
+ startY.current = 0;
281
+ }, [disabled, isRefreshing, pullDistance, pullThreshold, onRefresh]);
282
+
283
+ return {
284
+ pullDistance,
285
+ isRefreshing,
286
+ isReady: pullDistance >= pullThreshold,
287
+ progress: Math.min(pullDistance / pullThreshold, 1),
288
+ bind: {
289
+ onTouchStart: handleTouchStart,
290
+ onTouchMove: handleTouchMove,
291
+ onTouchEnd: handleTouchEnd,
292
+ },
293
+ };
294
+ }
@@ -37,7 +37,7 @@ export interface QueryTransparencyInfo {
37
37
  relatedData?: Array<{
38
38
  entity: string;
39
39
  description: string;
40
- type: 'join' | 'include' | 'lookup';
40
+ type: 'primary' | 'join' | 'include' | 'lookup';
41
41
  }>;
42
42
  // Backend calculations
43
43
  calculations?: Array<{