@spacecorexdev/core 0.0.4 → 0.0.5

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 (132) hide show
  1. package/package.json +2 -36
  2. package/src/api/_base.ts +243 -0
  3. package/src/api/index.ts +17 -0
  4. package/src/api/project.ts +1345 -0
  5. package/src/api/user.ts +1174 -0
  6. package/src/contract.ts +50 -0
  7. package/src/hooks/index.ts +2 -0
  8. package/{dist/hooks/index.js → src/hooks/useLongPress.ts} +53 -49
  9. package/src/hooks/useOnlineStatus.ts +22 -0
  10. package/src/index.ts +14 -0
  11. package/src/types/id.ts +2 -0
  12. package/src/types/index.ts +5 -0
  13. package/src/types/project/base.ts +63 -0
  14. package/src/types/project/contract.ts +79 -0
  15. package/src/types/project/contractStatus.tsx +15 -0
  16. package/src/types/project/index.ts +5 -0
  17. package/src/types/project/member.ts +46 -0
  18. package/src/types/project/progress.ts +43 -0
  19. package/src/types/projectRole.ts +17 -0
  20. package/src/types/theme.ts +1 -0
  21. package/src/types/user/index.ts +1 -0
  22. package/src/types/user/role.ts +7 -0
  23. package/src/ui/CommentTimeline/index.tsx +330 -0
  24. package/src/ui/FileManager/FileContextMenu.tsx +628 -0
  25. package/src/ui/FileManager/FileGridView.tsx +529 -0
  26. package/src/ui/FileManager/FileTableView.tsx +405 -0
  27. package/src/ui/FileManager/FileToolbar.tsx +119 -0
  28. package/src/ui/FileManager/FileVersionHistoryModal.tsx +175 -0
  29. package/src/ui/FileManager/MoveCopyToProjectModal.tsx +119 -0
  30. package/src/ui/FileManager/ShareToWechatModal.tsx +419 -0
  31. package/src/ui/FileManager/hooks/useFileManagerActions.ts +2532 -0
  32. package/src/ui/FileManager/hooks/useFileManagerState.ts +419 -0
  33. package/src/ui/FileManager/index.tsx +580 -0
  34. package/src/ui/FileManager/projectFileManagerApi.ts +57 -0
  35. package/src/ui/FileManager/types.ts +184 -0
  36. package/src/ui/FileManager/utils/fileDeltaWorker.ts +193 -0
  37. package/src/ui/FileManager/utils/fileTree.ts +178 -0
  38. package/src/ui/FileManager/utils/fileUpload.ts +714 -0
  39. package/src/ui/FileManager/utils/fileUtils.ts +65 -0
  40. package/src/ui/InvoiceRecordsList/index.tsx +813 -0
  41. package/src/ui/MyFileSelectorModal.tsx +261 -0
  42. package/src/ui/ProjectSelector/index.tsx +472 -0
  43. package/src/ui/ProjectSelector/projectCache.ts +40 -0
  44. package/src/ui/RoleSelector/index.tsx +473 -0
  45. package/src/ui/RoleSelector/roleCache.ts +40 -0
  46. package/src/ui/UIAlert.tsx +399 -0
  47. package/src/ui/UIAlertExample.tsx +129 -0
  48. package/src/ui/UIConfirm.tsx +335 -0
  49. package/src/ui/UIConfirmExample.tsx +165 -0
  50. package/src/ui/UIDatePicker.tsx +146 -0
  51. package/src/ui/UIDateRangePicker.tsx +38 -0
  52. package/src/ui/UIFilePreview/ExcelEditor/index.tsx +617 -0
  53. package/src/ui/UIFilePreview/ExcelEditor/types.ts +24 -0
  54. package/src/ui/UIFilePreview/ExportDropdown.css +85 -0
  55. package/src/ui/UIFilePreview/ExportDropdown.tsx +109 -0
  56. package/src/ui/UIFilePreview/IndexedDBUtils.ts +564 -0
  57. package/src/ui/UIFilePreview/MarkdownEditor/exportMarkdownPdf.ts +576 -0
  58. package/src/ui/UIFilePreview/MarkdownEditor/index.tsx +677 -0
  59. package/src/ui/UIFilePreview/MarkdownEditor/markdownEditor.css +1005 -0
  60. package/src/ui/UIFilePreview/MarkdownEditor/types.ts +15 -0
  61. package/src/ui/UIFilePreview/PPTXEditor/index.tsx +602 -0
  62. package/src/ui/UIFilePreview/RichDocEditor/exportRDocMarkdown.ts +77 -0
  63. package/src/ui/UIFilePreview/RichDocEditor/exportRDocPdf.ts +374 -0
  64. package/src/ui/UIFilePreview/RichDocEditor/index.tsx +730 -0
  65. package/src/ui/UIFilePreview/RichDocEditor/richdoc-editor.css +59 -0
  66. package/src/ui/UIFilePreview/UIFilePreview.tsx +611 -0
  67. package/src/ui/UIFilePreview/WordEditor/index.tsx +3933 -0
  68. package/src/ui/UIFilePreview/WordEditor/style.ts +1213 -0
  69. package/src/ui/UIFilePreview/WordEditor/word-editor.css +999 -0
  70. package/src/ui/UIFilePreview/filePreview.css +193 -0
  71. package/src/ui/UIFilePreview/filetypes/archive.tsx +66 -0
  72. package/src/ui/UIFilePreview/filetypes/cad.tsx +287 -0
  73. package/src/ui/UIFilePreview/filetypes/excel.tsx +355 -0
  74. package/src/ui/UIFilePreview/filetypes/image.tsx +20 -0
  75. package/src/ui/UIFilePreview/filetypes/imageLoader.ts +63 -0
  76. package/src/ui/UIFilePreview/filetypes/pdf.tsx +156 -0
  77. package/src/ui/UIFilePreview/filetypes/ppt.tsx +535 -0
  78. package/src/ui/UIFilePreview/filetypes/pptExportPdf.ts +523 -0
  79. package/src/ui/UIFilePreview/filetypes/video.tsx +482 -0
  80. package/src/ui/UIFilePreview/filetypes/word.tsx +245 -0
  81. package/src/ui/UIFilePreview/filetypes/wordExportMarkdown.ts +86 -0
  82. package/src/ui/UIFilePreview/filetypes/wordExportPdf.tsx +427 -0
  83. package/src/ui/UIFilePreview/index.ts +35 -0
  84. package/src/ui/UIFilePreview/pdfPagination.ts +143 -0
  85. package/src/ui/UIFilePreview/previewUtils.tsx +531 -0
  86. package/src/ui/UIFileUpload.tsx +382 -0
  87. package/src/ui/UIImageUpload.tsx +195 -0
  88. package/src/ui/UILoading.tsx +255 -0
  89. package/src/ui/UILoadingExample.tsx +139 -0
  90. package/src/ui/UISelect.tsx +105 -0
  91. package/src/ui/UIToast.tsx +381 -0
  92. package/src/ui/UIToastExample.tsx +69 -0
  93. package/src/ui/UIToastSingle.tsx +279 -0
  94. package/src/ui/UIViewToggle.tsx +64 -0
  95. package/src/ui/UserSelector/index.tsx +583 -0
  96. package/src/ui/index.ts +84 -0
  97. package/src/ui/upload/image.ts +135 -0
  98. package/src/ui/upload/index.ts +73 -0
  99. package/src/ui/upload/submitFile.ts +106 -0
  100. package/src/ui/upload/submitForm.ts +124 -0
  101. package/src/ui/upload/upload.err.ts +15 -0
  102. package/src/ui/upload/upload.ts +250 -0
  103. package/src/utils/CacheDepartmentInfo.ts +372 -0
  104. package/src/utils/CacheFileContent.ts +315 -0
  105. package/src/utils/CachePermissionInfo.ts +235 -0
  106. package/src/utils/CacheRoleInfo.ts +303 -0
  107. package/src/utils/CacheUserInfo.ts +818 -0
  108. package/src/utils/CacheViewToggleInfo.ts +182 -0
  109. package/src/utils/MessageContentUtils.ts +268 -0
  110. package/src/utils/clipboard.ts +70 -0
  111. package/src/utils/companyEmail.ts +19 -0
  112. package/src/utils/forbidDebugger.ts +59 -0
  113. package/src/utils/formatters.ts +94 -0
  114. package/src/utils/htmlUtils.tsx +59 -0
  115. package/src/utils/index.ts +18 -0
  116. package/src/utils/keyboard.ts +114 -0
  117. package/src/utils/mammothApi.ts +16 -0
  118. package/src/utils/polyfills.ts +928 -0
  119. package/src/utils/regex.ts +14 -0
  120. package/src/utils/storage.ts +95 -0
  121. package/src/utils/workerBootstrap.ts +87 -0
  122. package/dist/api/index.js +0 -1553
  123. package/dist/contract.js +0 -5
  124. package/dist/index.js +0 -4492
  125. package/dist/types/index.js +0 -49
  126. package/dist/ui/index.css +0 -1876
  127. package/dist/ui/index.js +0 -22245
  128. package/dist/utils/index.js +0 -4296
  129. /package/{dist → src}/styles/ant-modal.css +0 -0
  130. /package/{dist → src}/styles/ant-spin.css +0 -0
  131. /package/{dist → src}/styles/ant-table.css +0 -0
  132. /package/{dist → src}/styles/index.css +0 -0
@@ -0,0 +1,255 @@
1
+ /* eslint-disable react-refresh/only-export-components */
2
+ /**
3
+ * UILoading - 全局加载组件
4
+ *
5
+ * 使用示例:
6
+ *
7
+ * 1. 直接调用 API(推荐):
8
+ * import uiLoading from './UILoading';
9
+ *
10
+ * // 显示加载
11
+ * uiLoading.show({
12
+ * tip: '加载中...',
13
+ * fullscreen: true,
14
+ * mask: true,
15
+ * delay: 0 // 延迟显示时间(毫秒)
16
+ * });
17
+ *
18
+ * // 隐藏加载
19
+ * uiLoading.hide();
20
+ *
21
+ * // 使用 service 方法自动管理
22
+ * const fetchData = async () => {
23
+ * const data = await uiLoading.service(
24
+ * fetch('/api/data'),
25
+ * { tip: '正在获取数据...' }
26
+ * );
27
+ * return data;
28
+ * };
29
+ *
30
+ * // 多次调用,会计数,只有最后一个 hide 才会真正关闭
31
+ * uiLoading.show();
32
+ * uiLoading.show();
33
+ * uiLoading.hide(); // 计数减 1,仍显示
34
+ * uiLoading.hide(); // 计数减 1,关闭
35
+ *
36
+ * 2. 使用组件形式(较少使用):
37
+ * import { UILoading } from './UILoading';
38
+ *
39
+ * <UILoading tip="加载中..." />
40
+ *
41
+ * 特点:
42
+ * - 支持计数,多次 show 需要对应次数的 hide
43
+ * - 支持延迟显示,避免闪烁
44
+ * - service 方法自动在 Promise 完成后隐藏
45
+ */
46
+
47
+ /**
48
+ * 同时只能有一个timeout
49
+ */
50
+
51
+ import React from 'react';
52
+ import { Spin } from 'antd';
53
+ import { LoadingOutlined } from '@ant-design/icons';
54
+ import { createRoot, Root } from 'react-dom/client';
55
+
56
+ const loadingSymbol = Symbol('loading');
57
+
58
+ interface LoadingGlobalProps {
59
+ count: number;
60
+ timeoutId: number;
61
+ inTimeout: boolean;
62
+ }
63
+
64
+ function getLoadingGlobalProps() {
65
+ let props: LoadingGlobalProps = (window as any)[loadingSymbol] as LoadingGlobalProps;
66
+ if (!props) {
67
+ props = { count: 0, timeoutId: 0, inTimeout: false };
68
+ (window as any)[loadingSymbol] = props;
69
+ }
70
+ return props;
71
+ }
72
+
73
+
74
+
75
+ // 加载选项接口
76
+ export interface UILoadingOptions {
77
+ tip?: string;
78
+ fullscreen?: boolean;
79
+ delay?: number;
80
+ mask?: boolean;
81
+ maskStyle?: React.CSSProperties;
82
+ iconSize?: number;
83
+ iconStyle?: React.CSSProperties;
84
+ containerStyle?: React.CSSProperties;
85
+ wrapperClassName?: string;
86
+ }
87
+
88
+ // 默认的Loading选项
89
+ const defaultOptions: UILoadingOptions = {
90
+ tip: '',
91
+ fullscreen: true,
92
+ delay: 0,
93
+ mask: true,
94
+ iconSize: 40,
95
+ wrapperClassName: 'ui-loading-wrapper',
96
+ };
97
+
98
+ // 创建Loading组件
99
+ export const UILoading: React.FC<UILoadingOptions> = (props) => {
100
+ const options = { ...defaultOptions, ...props };
101
+ const {
102
+ tip,
103
+ fullscreen,
104
+ mask,
105
+ maskStyle,
106
+ iconSize,
107
+ iconStyle,
108
+ containerStyle,
109
+ wrapperClassName
110
+ } = options;
111
+
112
+ // 自定义加载图标
113
+ const loadingIcon = (
114
+ <LoadingOutlined
115
+ style={{
116
+ fontSize: iconSize,
117
+ color: '#1677ff',
118
+ ...iconStyle
119
+ }}
120
+ spin
121
+ />
122
+ );
123
+
124
+ // 全屏加载样式
125
+ const fullscreenStyle: React.CSSProperties = fullscreen ? {
126
+ position: 'fixed',
127
+ top: 0,
128
+ left: 0,
129
+ right: 0,
130
+ bottom: 0,
131
+ zIndex: 9999,
132
+ display: 'flex',
133
+ justifyContent: 'center',
134
+ alignItems: 'center',
135
+ backgroundColor: mask ? 'rgba(0, 0, 0, 0.45)' : 'transparent',
136
+ ...maskStyle
137
+ } : {};
138
+
139
+ // 容器样式
140
+ const containerStyles: React.CSSProperties = {
141
+ display: 'flex',
142
+ flexDirection: 'column',
143
+ alignItems: 'center',
144
+ justifyContent: 'center',
145
+ ...containerStyle
146
+ };
147
+
148
+ return (
149
+ <div style={fullscreenStyle} className={wrapperClassName}>
150
+ <div style={containerStyles}>
151
+ <Spin indicator={loadingIcon}>
152
+ {tip}
153
+ </Spin>
154
+ </div>
155
+ </div>
156
+ );
157
+ };
158
+
159
+ // 单例管理
160
+ let loadingInstance: HTMLDivElement | null = null;
161
+ let rootInstance: Root | null = null;
162
+
163
+ // 创建Loading实例
164
+ const createLoadingInstance = (options: UILoadingOptions = {}) => {
165
+ // 如果已存在实例,则先移除
166
+ if (loadingInstance && rootInstance) {
167
+ rootInstance.unmount();
168
+ document.body.removeChild(loadingInstance);
169
+ loadingInstance = null;
170
+ rootInstance = null;
171
+ }
172
+
173
+ // 创建挂载点
174
+ loadingInstance = document.createElement('div');
175
+ document.body.appendChild(loadingInstance);
176
+
177
+ // 使用createRoot API渲染组件
178
+ rootInstance = createRoot(loadingInstance);
179
+ rootInstance.render(<UILoading {...options} />);
180
+
181
+ return loadingInstance;
182
+ };
183
+
184
+ // 全局Loading API
185
+ const uiLoading = {
186
+ // 显示加载
187
+ show: (options: UILoadingOptions = {}) => {
188
+ let props = getLoadingGlobalProps();
189
+ props.count++;
190
+
191
+ const mergedOptions = { ...defaultOptions, ...options };
192
+
193
+ // 如果设置了延迟显示
194
+ if (mergedOptions.delay && mergedOptions.delay > 0) {
195
+ if (props.timeoutId != 0) {
196
+ window.clearTimeout(props.timeoutId);
197
+ props.timeoutId = 0;
198
+ }
199
+
200
+ props.timeoutId = window.setTimeout(() => {
201
+ props.timeoutId = 0;
202
+ createLoadingInstance(mergedOptions);
203
+ }, mergedOptions.delay);
204
+ return;
205
+ }
206
+ else {
207
+ createLoadingInstance(mergedOptions);
208
+ }
209
+ },
210
+
211
+ // 隐藏加载
212
+ hide: () => {
213
+ let props = getLoadingGlobalProps();
214
+ if (props.count <= 0) {
215
+ console.warn('Loading count is less than 0');
216
+ return;
217
+ }
218
+
219
+ props.count--;
220
+
221
+ // 清除延迟显示的定时器
222
+ if (props.timeoutId != 0) {
223
+ window.clearTimeout(props.timeoutId);
224
+ props.timeoutId = 0;
225
+ }
226
+
227
+ // 移除Loading实例
228
+ if (props.count == 0 && loadingInstance && rootInstance) {
229
+ rootInstance.unmount();
230
+ document.body.removeChild(loadingInstance);
231
+ loadingInstance = null;
232
+ rootInstance = null;
233
+ }
234
+ },
235
+
236
+ // 服务方法 - 自动在Promise完成后隐藏
237
+ service: <T,>(
238
+ promise: Promise<T>,
239
+ options: UILoadingOptions = {}
240
+ ): Promise<T> => {
241
+ uiLoading.show(options);
242
+
243
+ return promise
244
+ .then((res) => {
245
+ uiLoading.hide();
246
+ return res;
247
+ })
248
+ .catch((err) => {
249
+ uiLoading.hide();
250
+ throw err;
251
+ });
252
+ }
253
+ };
254
+
255
+ export default uiLoading;
@@ -0,0 +1,139 @@
1
+ import React, { useState } from 'react';
2
+ import { Button, Space, Card, Divider, Select } from 'antd';
3
+ import uiLoading from './UILoading';
4
+
5
+ const LoadingExample: React.FC = () => {
6
+ const [delay, setDelay] = useState<number>(0);
7
+
8
+ // 简单加载示例
9
+ const showSimpleLoading = () => {
10
+ uiLoading.show();
11
+
12
+ // 3秒后自动隐藏
13
+ setTimeout(() => {
14
+ uiLoading.hide();
15
+ }, 3000);
16
+ };
17
+
18
+ // 自定义文字加载
19
+ const showCustomTipLoading = () => {
20
+ uiLoading.show({
21
+ tip: '数据加载中,请稍候...',
22
+ });
23
+
24
+ // 3秒后自动隐藏
25
+ setTimeout(() => {
26
+ uiLoading.hide();
27
+ }, 3000);
28
+ };
29
+
30
+ // 无遮罩层加载
31
+ const showNoMaskLoading = () => {
32
+ uiLoading.show({
33
+ mask: false,
34
+ tip: '透明背景加载',
35
+ });
36
+
37
+ // 3秒后自动隐藏
38
+ setTimeout(() => {
39
+ uiLoading.hide();
40
+ }, 3000);
41
+ };
42
+
43
+ // 延迟显示加载
44
+ const showDelayedLoading = () => {
45
+ uiLoading.show({
46
+ delay,
47
+ tip: `${delay}毫秒后显示`,
48
+ });
49
+
50
+ // 3秒后自动隐藏
51
+ setTimeout(() => {
52
+ uiLoading.hide();
53
+ }, 3000 + delay);
54
+ };
55
+
56
+ // 处理Promise的加载示例
57
+ const handlePromiseLoading = () => {
58
+ // 模拟异步请求
59
+ const mockAsyncRequest = () => {
60
+ return new Promise((resolve) => {
61
+ setTimeout(() => {
62
+ resolve('请求成功');
63
+ }, 2000);
64
+ });
65
+ };
66
+
67
+ // 使用service方法自动处理Loading的显示和隐藏
68
+ uiLoading.service(
69
+ mockAsyncRequest(),
70
+ { tip: '' }
71
+ ).then((result) => {
72
+ console.log(result);
73
+ });
74
+ };
75
+
76
+ // 使用自定义样式加载
77
+ const showCustomStyledLoading = () => {
78
+ uiLoading.show({
79
+ tip: '自定义样式',
80
+ iconSize: 60,
81
+ iconStyle: { color: '#ff4d4f' },
82
+ maskStyle: { backgroundColor: 'rgba(0, 0, 0, 0.7)' },
83
+ });
84
+
85
+ // 3秒后自动隐藏
86
+ setTimeout(() => {
87
+ uiLoading.hide();
88
+ }, 3000);
89
+ };
90
+
91
+ return (
92
+ <Card title="Loading 加载示例" style={{ width: '100%', maxWidth: 600, margin: '20px auto' }}>
93
+ <Space direction="vertical" style={{ width: '100%' }}>
94
+ <Button type="primary" block onClick={showSimpleLoading}>
95
+ 显示基本加载
96
+ </Button>
97
+
98
+ <Button block onClick={showCustomTipLoading}>
99
+ 显示自定义文字加载
100
+ </Button>
101
+
102
+ <Button block onClick={showNoMaskLoading}>
103
+ 显示无遮罩层加载
104
+ </Button>
105
+
106
+ <Divider>延迟加载</Divider>
107
+
108
+ <Space style={{ width: '100%' }}>
109
+ <Select
110
+ value={delay}
111
+ onChange={(value) => setDelay(value)}
112
+ options={[
113
+ { value: 0, label: '不延迟' },
114
+ { value: 500, label: '延迟500ms' },
115
+ { value: 1000, label: '延迟1000ms' },
116
+ { value: 2000, label: '延迟2000ms' },
117
+ ]}
118
+ style={{ width: 120 }}
119
+ />
120
+ <Button type="dashed" onClick={showDelayedLoading}>
121
+ 延迟显示加载
122
+ </Button>
123
+ </Space>
124
+
125
+ <Divider>高级用法</Divider>
126
+
127
+ <Button type="primary" ghost block onClick={handlePromiseLoading}>
128
+ Promise自动加载
129
+ </Button>
130
+
131
+ <Button danger block onClick={showCustomStyledLoading}>
132
+ 自定义样式加载
133
+ </Button>
134
+ </Space>
135
+ </Card>
136
+ );
137
+ };
138
+
139
+ export default LoadingExample;
@@ -0,0 +1,105 @@
1
+ /**
2
+ * UISelect - 自定义下拉选择组件
3
+ *
4
+ * 使用示例:
5
+ *
6
+ * 1. 基础用法:
7
+ * import { UISelect } from './UISelect';
8
+ *
9
+ * const [value, setValue] = useState('');
10
+ *
11
+ * <UISelect
12
+ * options={[
13
+ * { value: 'option1', label: '选项1' },
14
+ * { value: 'option2', label: '选项2' },
15
+ * { value: 'option3', label: '选项3' }
16
+ * ]}
17
+ * value={value}
18
+ * onChange={setValue}
19
+ * placeholder="请选择"
20
+ * />
21
+ *
22
+ * 特点:
23
+ * - 支持点击外部关闭
24
+ * - 支持自定义样式
25
+ * - iOS 风格设计
26
+ */
27
+
28
+ import React, { useState, useRef, useEffect } from 'react';
29
+ import { ChevronDown } from 'lucide-react';
30
+ import { Button } from 'antd';
31
+
32
+ interface UISelectOption {
33
+ value: string;
34
+ label: string;
35
+ }
36
+
37
+ interface UISelectProps {
38
+ options: UISelectOption[];
39
+ value: string;
40
+ onChange: (value: string) => void;
41
+ placeholder?: string;
42
+ className?: string;
43
+ }
44
+
45
+ export function UISelect({ options, value, onChange, placeholder = '请选择', className = '' }: UISelectProps) {
46
+ const [isOpen, setIsOpen] = useState(false);
47
+ const selectRef = useRef<HTMLDivElement>(null);
48
+ const selectedOption = options.find(option => option.value === value);
49
+
50
+ useEffect(() => {
51
+ function handleClickOutside(event: MouseEvent) {
52
+ if (selectRef.current && !selectRef.current.contains(event.target as Node)) {
53
+ setIsOpen(false);
54
+ }
55
+ }
56
+
57
+ document.addEventListener('mousedown', handleClickOutside);
58
+ return () => document.removeEventListener('mousedown', handleClickOutside);
59
+ }, []);
60
+
61
+ return (
62
+ <div className={`relative ${className}`} ref={selectRef}>
63
+ <Button
64
+ type="text"
65
+ onClick={() => setIsOpen(!isOpen)}
66
+ className="w-full bg-white border border-ios-gray-200 rounded-ios py-2.5 pl-3 pr-10 text-left shadow-ios focus:outline-none focus:ring-2 focus:ring-ios-blue-400 focus:border-transparent transition-all duration-200 hover:border-ios-blue-300"
67
+ >
68
+ <span className="block truncate text-sm text-ios-gray-700">
69
+ {selectedOption ? selectedOption.label : placeholder}
70
+ </span>
71
+ <span className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
72
+ <ChevronDown className={`h-4 w-4 text-ios-gray-400 transition-transform duration-200 ${isOpen ? 'transform rotate-180' : ''}`} />
73
+ </span>
74
+ </Button>
75
+
76
+ {isOpen && (
77
+ <div className="absolute z-10 mt-1 w-full bg-white shadow-ios-md max-h-60 rounded-ios py-1 text-base ring-1 ring-ios-gray-200 ring-opacity-5 overflow-auto focus:outline-none sm:text-sm">
78
+ {options.map((option) => (
79
+ <div
80
+ key={option.value}
81
+ className={`cursor-pointer select-none relative py-2.5 pl-3 pr-9 hover:bg-ios-blue-50 transition-colors duration-150 ${
82
+ option.value === value ? 'bg-ios-blue-50 text-ios-blue-600 font-medium' : 'text-ios-gray-700'
83
+ }`}
84
+ onClick={() => {
85
+ onChange(option.value);
86
+ setIsOpen(false);
87
+ }}
88
+ >
89
+ <span className="block truncate text-sm">
90
+ {option.label}
91
+ </span>
92
+ {option.value === value && (
93
+ <span className="absolute inset-y-0 right-0 flex items-center pr-4 text-ios-blue-500">
94
+ <svg className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
95
+ <path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
96
+ </svg>
97
+ </span>
98
+ )}
99
+ </div>
100
+ ))}
101
+ </div>
102
+ )}
103
+ </div>
104
+ );
105
+ }