lupine.components 1.1.55 → 1.1.56

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 (136) hide show
  1. package/LICENSE +21 -21
  2. package/package.json +1 -1
  3. package/src/component-pool/aspect-ratio/aspect-ratio-demo.tsx +103 -103
  4. package/src/component-pool/avatar/avatar-demo.tsx +127 -127
  5. package/src/component-pool/badge/badge-demo.tsx +152 -152
  6. package/src/component-pool/breadcrumbs/breadcrumbs-demo.tsx +105 -105
  7. package/src/component-pool/card/card-demo.tsx +179 -179
  8. package/src/component-pool/carousel/carousel-demo.tsx +191 -191
  9. package/src/component-pool/cascader/cascader-demo.tsx +127 -127
  10. package/src/component-pool/charts/area-chart-demo.tsx +106 -106
  11. package/src/component-pool/charts/area-chart.tsx +254 -254
  12. package/src/component-pool/charts/bar-chart-demo.tsx +107 -107
  13. package/src/component-pool/charts/bar-chart.tsx +223 -223
  14. package/src/component-pool/charts/chart-utils.ts +182 -182
  15. package/src/component-pool/charts/column-chart-demo.tsx +111 -111
  16. package/src/component-pool/charts/column-chart.tsx +230 -230
  17. package/src/component-pool/charts/donut-chart-demo.tsx +112 -112
  18. package/src/component-pool/charts/donut-chart.tsx +6 -6
  19. package/src/component-pool/charts/gauge-chart-demo.tsx +101 -101
  20. package/src/component-pool/charts/gauge-chart.tsx +117 -117
  21. package/src/component-pool/charts/index.ts +19 -19
  22. package/src/component-pool/charts/line-chart-demo.tsx +109 -109
  23. package/src/component-pool/charts/line-chart.tsx +233 -233
  24. package/src/component-pool/charts/pie-chart-demo.tsx +133 -133
  25. package/src/component-pool/charts/pie-chart.tsx +142 -142
  26. package/src/component-pool/charts/radar-chart-demo.tsx +88 -88
  27. package/src/component-pool/charts/radar-chart.tsx +231 -231
  28. package/src/component-pool/charts/scatter-chart-demo.tsx +88 -88
  29. package/src/component-pool/charts/scatter-chart.tsx +237 -237
  30. package/src/component-pool/copy-button/copy-button-demo.tsx +71 -71
  31. package/src/component-pool/date-picker/date-picker-demo.tsx +84 -84
  32. package/src/component-pool/date-picker/date-picker.tsx +437 -437
  33. package/src/component-pool/date-picker/index.ts +2 -2
  34. package/src/component-pool/floating-icon-menu/floating-icon-menu-demo.tsx +94 -94
  35. package/src/component-pool/floating-icon-menu/floating-icon-menu.tsx +241 -241
  36. package/src/component-pool/floating-icon-menu/index.ts +2 -2
  37. package/src/component-pool/h-editor/h-editor-demo.tsx +112 -112
  38. package/src/component-pool/h-editor/h-editor.ts +862 -862
  39. package/src/component-pool/h-editor/index.ts +2 -2
  40. package/src/component-pool/i-editor/i-editor-demo.tsx +72 -72
  41. package/src/component-pool/i-editor/i-editor-frames.ts +42 -42
  42. package/src/component-pool/i-editor/i-editor-styles.ts +22 -22
  43. package/src/component-pool/i-editor/i-editor.ts +3869 -3869
  44. package/src/component-pool/i-editor/index.ts +2 -2
  45. package/src/component-pool/map-wrapper/map-wrapper-demo.tsx +55 -55
  46. package/src/component-pool/p-editor/index.ts +4 -4
  47. package/src/component-pool/p-editor/p-editor-demo.tsx +51 -51
  48. package/src/component-pool/p-editor/p-editor-styles.ts +44 -44
  49. package/src/component-pool/p-editor/p-editor-types.ts +4 -4
  50. package/src/component-pool/p-editor/p-editor-utils.ts +38 -38
  51. package/src/component-pool/p-editor/p-editor.ts +2477 -2477
  52. package/src/component-pool/pdf-viewer/index.ts +2 -2
  53. package/src/component-pool/pdf-viewer/pdf-viewer-demo.tsx +116 -116
  54. package/src/component-pool/pdf-viewer/pdf-viewer.tsx +56 -56
  55. package/src/component-pool/picker-helper.tsx +222 -222
  56. package/src/component-pool/pull-to-refresh/pull-to-refresh-demo.tsx +99 -99
  57. package/src/component-pool/qrcode/index.ts +3 -3
  58. package/src/component-pool/qrcode/qrcode-demo.tsx +102 -102
  59. package/src/component-pool/qrcode/qrcode.tsx +169 -169
  60. package/src/component-pool/radial-progress/index.ts +2 -2
  61. package/src/component-pool/radial-progress/radial-progress-demo.tsx +84 -84
  62. package/src/component-pool/radial-progress/radial-progress.tsx +118 -118
  63. package/src/component-pool/range/gauge-demo.tsx +113 -113
  64. package/src/component-pool/range/gauge.tsx +392 -392
  65. package/src/component-pool/range/range-demo.tsx +97 -97
  66. package/src/component-pool/search-input/index.ts +2 -2
  67. package/src/component-pool/search-input/search-input-demo.tsx +50 -50
  68. package/src/component-pool/search-input/search-input.tsx +125 -125
  69. package/src/component-pool/skeleton/skeleton-card.tsx +114 -114
  70. package/src/component-pool/skeleton/skeleton-demo.tsx +55 -55
  71. package/src/component-pool/svg-icons.ts +95 -95
  72. package/src/component-pool/svg-props.tsx +144 -144
  73. package/src/component-pool/tag-input/index.ts +2 -2
  74. package/src/component-pool/tag-input/tag-input-demo.tsx +47 -47
  75. package/src/component-pool/tag-input/tag-input.tsx +166 -166
  76. package/src/component-pool/time-picker/index.ts +2 -2
  77. package/src/component-pool/time-picker/time-picker-demo.tsx +80 -80
  78. package/src/component-pool/time-picker/time-picker.tsx +311 -311
  79. package/src/component-pool/timeline/timeline-demo.tsx +75 -75
  80. package/src/component-pool/tooltip/tooltip-demo.tsx +144 -144
  81. package/src/component-pool/tour/tour-demo.tsx +92 -92
  82. package/src/component-pool/youtube-player/index.ts +2 -2
  83. package/src/component-pool/youtube-player/youtube-player-demo.tsx +57 -57
  84. package/src/component-pool/youtube-player/youtube-player.tsx +116 -116
  85. package/src/components/action-sheet-color.tsx +269 -269
  86. package/src/components/action-sheet-date.tsx +299 -299
  87. package/src/components/action-sheet-demo.tsx +216 -216
  88. package/src/components/action-sheet-theme.tsx +150 -150
  89. package/src/components/action-sheet-time.tsx +157 -157
  90. package/src/components/input-number.tsx +206 -206
  91. package/src/components/input-with-title-demo.tsx +42 -42
  92. package/src/components/loading-spin.tsx +74 -74
  93. package/src/components/message-box-demo.tsx +107 -107
  94. package/src/components/mobile-components/mobile-side-menu-demo.tsx +47 -47
  95. package/src/components/modal-demo.tsx +56 -56
  96. package/src/components/notice-message-demo.tsx +57 -57
  97. package/src/components/paging-link-demo.tsx +430 -430
  98. package/src/components/popup-menu-demo.tsx +172 -172
  99. package/src/components/progress-demo.tsx +53 -53
  100. package/src/components/radio-label-demo.tsx +41 -41
  101. package/src/components/redirect-demo.tsx +36 -36
  102. package/src/components/resizable-splitter-demo.tsx +104 -104
  103. package/src/components/select-angle-demo.tsx +32 -32
  104. package/src/components/select-with-title-demo.tsx +53 -53
  105. package/src/components/slide-tab-component-demo.tsx +86 -86
  106. package/src/components/spinner-demo.tsx +52 -52
  107. package/src/components/stars-component-demo.tsx +36 -36
  108. package/src/components/switch-option-component.tsx +74 -74
  109. package/src/components/switch-option-demo.tsx +36 -36
  110. package/src/components/tabs-demo.tsx +42 -42
  111. package/src/components/text-glow-demo.tsx +46 -46
  112. package/src/components/text-scale-demo.tsx +41 -41
  113. package/src/components/text-wave-demo.tsx +48 -48
  114. package/src/components/toggle-button-demo.tsx +230 -230
  115. package/src/demo/demo-page.tsx +271 -271
  116. package/src/demo/demo-registry.ts +143 -143
  117. package/src/demo/demo-render-page.tsx +94 -94
  118. package/src/demo/demo-types.ts +22 -22
  119. package/src/demo/mock/demo-icons.ts +72 -72
  120. package/src/demo/mock/responsive-demo-mock-pages.tsx +197 -197
  121. package/src/demo/mock/side-menu-mock.tsx +113 -113
  122. package/src/demo/mock/user-settings-mock.tsx +310 -310
  123. package/src/frames/responsive-frame-demo.tsx +120 -120
  124. package/src/frames/slider-frame-demo.tsx +111 -111
  125. package/src/frames/slider-helper.tsx +194 -194
  126. package/src/lib/button-async-lock.tsx +43 -43
  127. package/src/lib/canvas/canvas-drawing.ts +503 -503
  128. package/src/lib/canvas/canvas-geometry.ts +255 -255
  129. package/src/lib/canvas/canvas-helper.tsx +176 -176
  130. package/src/lib/canvas/canvas-image.ts +110 -110
  131. package/src/lib/canvas/canvas-types.ts +93 -93
  132. package/src/lib/canvas/index.ts +5 -5
  133. package/src/lib/disable-console.ts +11 -11
  134. package/src/lib/disable-debug.ts +8 -8
  135. package/src/lib/support-safe-area.ts +25 -25
  136. package/tsconfig.json +113 -113
@@ -1,299 +1,299 @@
1
- import { CssProps, RefProps, VNode } from 'lupine.web';
2
- import { HtmlVar } from './html-var';
3
- import { ActionSheet, ActionSheetCloseProps, ActionSheetCloseReasonProps } from './action-sheet';
4
-
5
- export type ActionSheetDatePickerOrder = 'YMD' | 'DMY';
6
- export type ActionSheetDatePickerFormat = 'YMD' | 'MD' | 'D';
7
-
8
- export type ActionSheetDatePickerProps = {
9
- /** Initial date value. Format depends on format prop, defaults to "YYYY-MM-DD" */
10
- value?: string;
11
- title?: string;
12
- /** Format of the picker. Default: 'YMD' */
13
- format?: ActionSheetDatePickerFormat;
14
- /** Column order. Default: 'YMD' (Year | Month | Day) */
15
- order?: ActionSheetDatePickerOrder;
16
- /** Month labels. Defaults to English month names. Length determines month count. */
17
- months?: string[];
18
- /** Years shown above/below current year. Default: 100 (range = currentYear-N to currentYear+N) */
19
- yearRange?: number;
20
- confirmButtonText?: string;
21
- cancelButtonText?: string;
22
- zIndex?: string;
23
- };
24
-
25
- const DEFAULT_MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
26
-
27
- // Year range: ±yearRange from current year (resolved at call time)
28
- const CURRENT_YEAR = new Date().getFullYear();
29
-
30
- const ITEM_H = 44;
31
- const VISIBLE = 5; // number of visible rows (must be odd)
32
- const PAD = Math.floor(VISIBLE / 2); // 2 rows of padding above/below
33
-
34
- /**
35
- * Mobile-friendly date picker shown via an ActionSheet slide-up panel.
36
- * Shows Year / Month / Day (or Day / Month / Year) columns.
37
- * Returns "YYYY-MM-DD" or undefined if cancelled.
38
- */
39
- export const ActionSheetDatePicker = async ({
40
- value = '',
41
- title = '',
42
- format = 'YMD',
43
- order = 'YMD',
44
- months = DEFAULT_MONTHS,
45
- yearRange = 100,
46
- confirmButtonText = 'OK',
47
- cancelButtonText = 'Cancel',
48
- zIndex,
49
- }: ActionSheetDatePickerProps): Promise<string | undefined> => {
50
- // Parse initial date
51
- const today = new Date();
52
-
53
- let parts: string[] = [];
54
- if (format === 'MD') {
55
- parts = value.match(/^(\d{2})-(\d{2})$/) ?? [];
56
- parts.unshift(''); // Shift empty year so indices [1]/[2] match month/day fallback logic below
57
- parts.unshift(''); // Padding to push match groups to [2][3]
58
- } else if (format === 'D') {
59
- parts = value.match(/^(\d{2})$/) ?? [];
60
- parts.unshift('');
61
- parts.unshift('');
62
- parts.unshift(''); // Push match group to [3]
63
- } else {
64
- parts = value.match(/^(\d{4})-(\d{2})-(\d{2})$/) ?? [];
65
- }
66
-
67
- const initialYear = parts[1] ? parseInt(parts[1], 10) : today.getFullYear();
68
- const initialMonth = parts[2] ? parseInt(parts[2], 10) : today.getMonth() + 1;
69
- const initialDay = parts[3] ? parseInt(parts[3], 10) : today.getDate();
70
-
71
- const YEAR_COUNT = yearRange * 2 + 1;
72
- const START_YEAR = CURRENT_YEAR - yearRange;
73
-
74
- return new Promise(async (resolve) => {
75
- let handleClose: ActionSheetCloseProps;
76
-
77
- // One ref per column — values are read from scrollTop at confirm time
78
- const yearRef: RefProps = {};
79
- const monthRef: RefProps = {};
80
- const dayRef: RefProps = {};
81
-
82
- const getIdx = (colRef: RefProps) => {
83
- if (!colRef.current) return 0;
84
- return Math.round((colRef.current as HTMLElement).scrollTop / ITEM_H);
85
- };
86
-
87
- const onConfirm = () => {
88
- const year = START_YEAR + getIdx(yearRef);
89
- const month = 1 + getIdx(monthRef);
90
- const day = 1 + getIdx(dayRef);
91
-
92
- let result = '';
93
- if (format === 'D') {
94
- result = String(day).padStart(2, '0');
95
- } else if (format === 'MD') {
96
- result = [String(month).padStart(2, '0'), String(day).padStart(2, '0')].join('-');
97
- } else {
98
- result = [String(year).padStart(4, '0'), String(month).padStart(2, '0'), String(day).padStart(2, '0')].join(
99
- '-'
100
- );
101
- }
102
-
103
- resolve(result);
104
- handleClose('confirm');
105
- };
106
- const closeEvent = (reason?: ActionSheetCloseReasonProps) => {
107
- if (reason !== 'confirm') resolve(undefined);
108
- };
109
-
110
- /**
111
- * Build a scrollable column.
112
- * Carousel pattern: scroll to item's offsetTop on load; read scrollTop on confirm.
113
- */
114
- const buildColumn = (labels: string[], initialIdx: number, colRef: RefProps, onScroll?: () => void) => {
115
- colRef.onLoad = async () => {
116
- const el = colRef.current as HTMLElement;
117
- const items = el.querySelectorAll('.dp-item');
118
- if (items[initialIdx]) {
119
- // offsetTop accounts for the PAD padding divs above the items
120
- el.scrollTop = (items[initialIdx] as HTMLElement).offsetTop - PAD * ITEM_H;
121
- }
122
- };
123
-
124
- return (
125
- <div class='col' ref={colRef} onScroll={onScroll}>
126
- {Array.from({ length: PAD }, (_, i) => (
127
- <div class='dp-pad' key={`t${i}`} />
128
- ))}
129
- {labels.map((label, i) => (
130
- <div class='dp-item' key={i}>
131
- {label}
132
- </div>
133
- ))}
134
- {Array.from({ length: PAD }, (_, i) => (
135
- <div class='dp-pad' key={`b${i}`} />
136
- ))}
137
- </div>
138
- );
139
- };
140
-
141
- const buildDynamicColumn = (labelsVar: HtmlVar, initialIdx: number, colRef: RefProps) => {
142
- colRef.onLoad = async () => {
143
- const el = colRef.current as HTMLElement;
144
- const items = el.querySelectorAll('.dp-item');
145
- if (items[initialIdx]) {
146
- el.scrollTop = (items[initialIdx] as HTMLElement).offsetTop - PAD * ITEM_H;
147
- }
148
- };
149
-
150
- return (
151
- <div class='col' ref={colRef}>
152
- {labelsVar.node}
153
- </div>
154
- );
155
- };
156
-
157
- const generateDaysDOM = (maxDays: number) => {
158
- const labels = Array.from({ length: maxDays }, (_, i) => String(i + 1).padStart(2, '0'));
159
- return (
160
- <>
161
- {Array.from({ length: PAD }, (_, i) => (
162
- <div class='dp-pad' key={`t${i}`} />
163
- ))}
164
- {labels.map((label: string, i: number) => (
165
- <div class='dp-item' key={i}>
166
- {label}
167
- </div>
168
- ))}
169
- {Array.from({ length: PAD }, (_, i) => (
170
- <div class='dp-pad' key={`b${i}`} />
171
- ))}
172
- </>
173
- );
174
- };
175
-
176
- const getMaxDays = (year: number, month: number) => {
177
- if (format === 'D') return 31;
178
- if (format === 'MD') return new Date(2000, month, 0).getDate(); // Leap year forced max
179
- return new Date(year, month, 0).getDate();
180
- };
181
-
182
- let currentMaxDays = 0;
183
- let scrollTimeout: any;
184
- const handleDateScroll = () => {
185
- clearTimeout(scrollTimeout);
186
- scrollTimeout = setTimeout(() => {
187
- // Read current selected year and month based on scroll heights
188
- const curYear = format === 'MD' ? 2000 : START_YEAR + getIdx(yearRef);
189
- const curMonth = 1 + getIdx(monthRef);
190
-
191
- // Calculate the maximum allowed days for this selection
192
- const maxDays = getMaxDays(curYear, curMonth);
193
-
194
- // If the number of days changed, update the reactive var
195
- if (currentMaxDays !== maxDays) {
196
- currentMaxDays = maxDays;
197
- daysVar.value = generateDaysDOM(maxDays);
198
-
199
- // If the currently selected day is now out of bounds (e.g. Feb 30th), clamp it down.
200
- const currentDayIdx = getIdx(dayRef);
201
- if (currentDayIdx >= maxDays) {
202
- setTimeout(() => {
203
- if (dayRef.current) {
204
- const el = dayRef.current as HTMLElement;
205
- const items = el.querySelectorAll('.dp-item');
206
- const targetItem = items[maxDays - 1] as HTMLElement;
207
- if (targetItem) {
208
- el.scrollTo({ top: targetItem.offsetTop - PAD * ITEM_H, behavior: 'smooth' });
209
- }
210
- }
211
- }, 50);
212
- }
213
- }
214
- }, 150);
215
- };
216
-
217
- const years = Array.from({ length: YEAR_COUNT }, (_, i) => String(START_YEAR + i));
218
-
219
- const initialYearIdx = Math.max(0, Math.min(YEAR_COUNT - 1, initialYear - START_YEAR));
220
- const initialMonthIdx = Math.max(0, Math.min(months.length - 1, initialMonth - 1));
221
- currentMaxDays = getMaxDays(START_YEAR + initialYearIdx, initialMonthIdx + 1);
222
-
223
- // Day array is reactive, max based on initial selection constraints
224
- const daysVar = new HtmlVar(generateDaysDOM(currentMaxDays));
225
- const initialDayIdx = Math.max(0, Math.min(currentMaxDays - 1, initialDay - 1));
226
-
227
- const yearCol = buildColumn(years, initialYearIdx, yearRef, handleDateScroll);
228
- const monthCol = buildColumn(months, initialMonthIdx, monthRef, handleDateScroll);
229
- const dayCol = buildDynamicColumn(daysVar, initialDayIdx, dayRef);
230
-
231
- let orderedCols: any[] = [];
232
- if (format === 'D') {
233
- orderedCols = [dayCol];
234
- } else if (format === 'MD') {
235
- orderedCols = order === 'DMY' ? [dayCol, monthCol] : [monthCol, dayCol];
236
- } else {
237
- orderedCols = order === 'DMY' ? [dayCol, monthCol, yearCol] : [yearCol, monthCol, dayCol];
238
- }
239
-
240
- const pickerBodyCss: CssProps = {
241
- borderTop: '1px solid var(--primary-border-color)',
242
- overflow: 'hidden', // prevent act-sheet-content from adding outer scrollbar
243
- '.picker-body': {
244
- display: 'flex',
245
- height: `${ITEM_H * VISIBLE}px`,
246
- position: 'relative',
247
- // Center-selection highlight band
248
- '&::before': {
249
- content: '""',
250
- position: 'absolute',
251
- top: `${ITEM_H * PAD}px`,
252
- left: 0,
253
- right: 0,
254
- height: `${ITEM_H}px`,
255
- backgroundColor: 'var(--activatable-bg-color-hover, rgba(0,0,0,0.05))',
256
- borderRadius: '6px',
257
- pointerEvents: 'none',
258
- opacity: 0.4,
259
- },
260
- },
261
- '.col': {
262
- flex: 1,
263
- overflowY: 'scroll',
264
- scrollSnapType: 'y mandatory',
265
- scrollbarWidth: 'none',
266
- '&::-webkit-scrollbar': { display: 'none' },
267
- '.dp-pad': { height: `${ITEM_H}px` },
268
- '.dp-item': {
269
- scrollSnapAlign: 'center',
270
- height: `${ITEM_H}px`,
271
- lineHeight: `${ITEM_H}px`,
272
- textAlign: 'center',
273
- fontSize: '18px',
274
- cursor: 'pointer',
275
- color: 'var(--secondary-color, #aaa)',
276
- },
277
- },
278
- };
279
-
280
- const pickerBody = (
281
- <div css={pickerBodyCss}>
282
- <div class='picker-body'>{orderedCols}</div>
283
- </div>
284
- );
285
-
286
- handleClose = await ActionSheet.show({
287
- title,
288
- children: pickerBody,
289
- contentMaxHeight: '33vh',
290
- closeEvent,
291
- closeWhenClickOutside: true,
292
- zIndex,
293
- confirmButtonText,
294
- handleConfirmClicked: onConfirm,
295
- cancelButtonText,
296
- buttonsPosition: 'header',
297
- });
298
- });
299
- };
1
+ import { CssProps, RefProps, VNode } from 'lupine.web';
2
+ import { HtmlVar } from './html-var';
3
+ import { ActionSheet, ActionSheetCloseProps, ActionSheetCloseReasonProps } from './action-sheet';
4
+
5
+ export type ActionSheetDatePickerOrder = 'YMD' | 'DMY';
6
+ export type ActionSheetDatePickerFormat = 'YMD' | 'MD' | 'D';
7
+
8
+ export type ActionSheetDatePickerProps = {
9
+ /** Initial date value. Format depends on format prop, defaults to "YYYY-MM-DD" */
10
+ value?: string;
11
+ title?: string;
12
+ /** Format of the picker. Default: 'YMD' */
13
+ format?: ActionSheetDatePickerFormat;
14
+ /** Column order. Default: 'YMD' (Year | Month | Day) */
15
+ order?: ActionSheetDatePickerOrder;
16
+ /** Month labels. Defaults to English month names. Length determines month count. */
17
+ months?: string[];
18
+ /** Years shown above/below current year. Default: 100 (range = currentYear-N to currentYear+N) */
19
+ yearRange?: number;
20
+ confirmButtonText?: string;
21
+ cancelButtonText?: string;
22
+ zIndex?: string;
23
+ };
24
+
25
+ const DEFAULT_MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
26
+
27
+ // Year range: ±yearRange from current year (resolved at call time)
28
+ const CURRENT_YEAR = new Date().getFullYear();
29
+
30
+ const ITEM_H = 44;
31
+ const VISIBLE = 5; // number of visible rows (must be odd)
32
+ const PAD = Math.floor(VISIBLE / 2); // 2 rows of padding above/below
33
+
34
+ /**
35
+ * Mobile-friendly date picker shown via an ActionSheet slide-up panel.
36
+ * Shows Year / Month / Day (or Day / Month / Year) columns.
37
+ * Returns "YYYY-MM-DD" or undefined if cancelled.
38
+ */
39
+ export const ActionSheetDatePicker = async ({
40
+ value = '',
41
+ title = '',
42
+ format = 'YMD',
43
+ order = 'YMD',
44
+ months = DEFAULT_MONTHS,
45
+ yearRange = 100,
46
+ confirmButtonText = 'OK',
47
+ cancelButtonText = 'Cancel',
48
+ zIndex,
49
+ }: ActionSheetDatePickerProps): Promise<string | undefined> => {
50
+ // Parse initial date
51
+ const today = new Date();
52
+
53
+ let parts: string[] = [];
54
+ if (format === 'MD') {
55
+ parts = value.match(/^(\d{2})-(\d{2})$/) ?? [];
56
+ parts.unshift(''); // Shift empty year so indices [1]/[2] match month/day fallback logic below
57
+ parts.unshift(''); // Padding to push match groups to [2][3]
58
+ } else if (format === 'D') {
59
+ parts = value.match(/^(\d{2})$/) ?? [];
60
+ parts.unshift('');
61
+ parts.unshift('');
62
+ parts.unshift(''); // Push match group to [3]
63
+ } else {
64
+ parts = value.match(/^(\d{4})-(\d{2})-(\d{2})$/) ?? [];
65
+ }
66
+
67
+ const initialYear = parts[1] ? parseInt(parts[1], 10) : today.getFullYear();
68
+ const initialMonth = parts[2] ? parseInt(parts[2], 10) : today.getMonth() + 1;
69
+ const initialDay = parts[3] ? parseInt(parts[3], 10) : today.getDate();
70
+
71
+ const YEAR_COUNT = yearRange * 2 + 1;
72
+ const START_YEAR = CURRENT_YEAR - yearRange;
73
+
74
+ return new Promise(async (resolve) => {
75
+ let handleClose: ActionSheetCloseProps;
76
+
77
+ // One ref per column — values are read from scrollTop at confirm time
78
+ const yearRef: RefProps = {};
79
+ const monthRef: RefProps = {};
80
+ const dayRef: RefProps = {};
81
+
82
+ const getIdx = (colRef: RefProps) => {
83
+ if (!colRef.current) return 0;
84
+ return Math.round((colRef.current as HTMLElement).scrollTop / ITEM_H);
85
+ };
86
+
87
+ const onConfirm = () => {
88
+ const year = START_YEAR + getIdx(yearRef);
89
+ const month = 1 + getIdx(monthRef);
90
+ const day = 1 + getIdx(dayRef);
91
+
92
+ let result = '';
93
+ if (format === 'D') {
94
+ result = String(day).padStart(2, '0');
95
+ } else if (format === 'MD') {
96
+ result = [String(month).padStart(2, '0'), String(day).padStart(2, '0')].join('-');
97
+ } else {
98
+ result = [String(year).padStart(4, '0'), String(month).padStart(2, '0'), String(day).padStart(2, '0')].join(
99
+ '-'
100
+ );
101
+ }
102
+
103
+ resolve(result);
104
+ handleClose('confirm');
105
+ };
106
+ const closeEvent = (reason?: ActionSheetCloseReasonProps) => {
107
+ if (reason !== 'confirm') resolve(undefined);
108
+ };
109
+
110
+ /**
111
+ * Build a scrollable column.
112
+ * Carousel pattern: scroll to item's offsetTop on load; read scrollTop on confirm.
113
+ */
114
+ const buildColumn = (labels: string[], initialIdx: number, colRef: RefProps, onScroll?: () => void) => {
115
+ colRef.onLoad = async () => {
116
+ const el = colRef.current as HTMLElement;
117
+ const items = el.querySelectorAll('.dp-item');
118
+ if (items[initialIdx]) {
119
+ // offsetTop accounts for the PAD padding divs above the items
120
+ el.scrollTop = (items[initialIdx] as HTMLElement).offsetTop - PAD * ITEM_H;
121
+ }
122
+ };
123
+
124
+ return (
125
+ <div class='col' ref={colRef} onScroll={onScroll}>
126
+ {Array.from({ length: PAD }, (_, i) => (
127
+ <div class='dp-pad' key={`t${i}`} />
128
+ ))}
129
+ {labels.map((label, i) => (
130
+ <div class='dp-item' key={i}>
131
+ {label}
132
+ </div>
133
+ ))}
134
+ {Array.from({ length: PAD }, (_, i) => (
135
+ <div class='dp-pad' key={`b${i}`} />
136
+ ))}
137
+ </div>
138
+ );
139
+ };
140
+
141
+ const buildDynamicColumn = (labelsVar: HtmlVar, initialIdx: number, colRef: RefProps) => {
142
+ colRef.onLoad = async () => {
143
+ const el = colRef.current as HTMLElement;
144
+ const items = el.querySelectorAll('.dp-item');
145
+ if (items[initialIdx]) {
146
+ el.scrollTop = (items[initialIdx] as HTMLElement).offsetTop - PAD * ITEM_H;
147
+ }
148
+ };
149
+
150
+ return (
151
+ <div class='col' ref={colRef}>
152
+ {labelsVar.node}
153
+ </div>
154
+ );
155
+ };
156
+
157
+ const generateDaysDOM = (maxDays: number) => {
158
+ const labels = Array.from({ length: maxDays }, (_, i) => String(i + 1).padStart(2, '0'));
159
+ return (
160
+ <>
161
+ {Array.from({ length: PAD }, (_, i) => (
162
+ <div class='dp-pad' key={`t${i}`} />
163
+ ))}
164
+ {labels.map((label: string, i: number) => (
165
+ <div class='dp-item' key={i}>
166
+ {label}
167
+ </div>
168
+ ))}
169
+ {Array.from({ length: PAD }, (_, i) => (
170
+ <div class='dp-pad' key={`b${i}`} />
171
+ ))}
172
+ </>
173
+ );
174
+ };
175
+
176
+ const getMaxDays = (year: number, month: number) => {
177
+ if (format === 'D') return 31;
178
+ if (format === 'MD') return new Date(2000, month, 0).getDate(); // Leap year forced max
179
+ return new Date(year, month, 0).getDate();
180
+ };
181
+
182
+ let currentMaxDays = 0;
183
+ let scrollTimeout: any;
184
+ const handleDateScroll = () => {
185
+ clearTimeout(scrollTimeout);
186
+ scrollTimeout = setTimeout(() => {
187
+ // Read current selected year and month based on scroll heights
188
+ const curYear = format === 'MD' ? 2000 : START_YEAR + getIdx(yearRef);
189
+ const curMonth = 1 + getIdx(monthRef);
190
+
191
+ // Calculate the maximum allowed days for this selection
192
+ const maxDays = getMaxDays(curYear, curMonth);
193
+
194
+ // If the number of days changed, update the reactive var
195
+ if (currentMaxDays !== maxDays) {
196
+ currentMaxDays = maxDays;
197
+ daysVar.value = generateDaysDOM(maxDays);
198
+
199
+ // If the currently selected day is now out of bounds (e.g. Feb 30th), clamp it down.
200
+ const currentDayIdx = getIdx(dayRef);
201
+ if (currentDayIdx >= maxDays) {
202
+ setTimeout(() => {
203
+ if (dayRef.current) {
204
+ const el = dayRef.current as HTMLElement;
205
+ const items = el.querySelectorAll('.dp-item');
206
+ const targetItem = items[maxDays - 1] as HTMLElement;
207
+ if (targetItem) {
208
+ el.scrollTo({ top: targetItem.offsetTop - PAD * ITEM_H, behavior: 'smooth' });
209
+ }
210
+ }
211
+ }, 50);
212
+ }
213
+ }
214
+ }, 150);
215
+ };
216
+
217
+ const years = Array.from({ length: YEAR_COUNT }, (_, i) => String(START_YEAR + i));
218
+
219
+ const initialYearIdx = Math.max(0, Math.min(YEAR_COUNT - 1, initialYear - START_YEAR));
220
+ const initialMonthIdx = Math.max(0, Math.min(months.length - 1, initialMonth - 1));
221
+ currentMaxDays = getMaxDays(START_YEAR + initialYearIdx, initialMonthIdx + 1);
222
+
223
+ // Day array is reactive, max based on initial selection constraints
224
+ const daysVar = new HtmlVar(generateDaysDOM(currentMaxDays));
225
+ const initialDayIdx = Math.max(0, Math.min(currentMaxDays - 1, initialDay - 1));
226
+
227
+ const yearCol = buildColumn(years, initialYearIdx, yearRef, handleDateScroll);
228
+ const monthCol = buildColumn(months, initialMonthIdx, monthRef, handleDateScroll);
229
+ const dayCol = buildDynamicColumn(daysVar, initialDayIdx, dayRef);
230
+
231
+ let orderedCols: any[] = [];
232
+ if (format === 'D') {
233
+ orderedCols = [dayCol];
234
+ } else if (format === 'MD') {
235
+ orderedCols = order === 'DMY' ? [dayCol, monthCol] : [monthCol, dayCol];
236
+ } else {
237
+ orderedCols = order === 'DMY' ? [dayCol, monthCol, yearCol] : [yearCol, monthCol, dayCol];
238
+ }
239
+
240
+ const pickerBodyCss: CssProps = {
241
+ borderTop: '1px solid var(--primary-border-color)',
242
+ overflow: 'hidden', // prevent act-sheet-content from adding outer scrollbar
243
+ '.picker-body': {
244
+ display: 'flex',
245
+ height: `${ITEM_H * VISIBLE}px`,
246
+ position: 'relative',
247
+ // Center-selection highlight band
248
+ '&::before': {
249
+ content: '""',
250
+ position: 'absolute',
251
+ top: `${ITEM_H * PAD}px`,
252
+ left: 0,
253
+ right: 0,
254
+ height: `${ITEM_H}px`,
255
+ backgroundColor: 'var(--activatable-bg-color-hover, rgba(0,0,0,0.05))',
256
+ borderRadius: '6px',
257
+ pointerEvents: 'none',
258
+ opacity: 0.4,
259
+ },
260
+ },
261
+ '.col': {
262
+ flex: 1,
263
+ overflowY: 'scroll',
264
+ scrollSnapType: 'y mandatory',
265
+ scrollbarWidth: 'none',
266
+ '&::-webkit-scrollbar': { display: 'none' },
267
+ '.dp-pad': { height: `${ITEM_H}px` },
268
+ '.dp-item': {
269
+ scrollSnapAlign: 'center',
270
+ height: `${ITEM_H}px`,
271
+ lineHeight: `${ITEM_H}px`,
272
+ textAlign: 'center',
273
+ fontSize: '18px',
274
+ cursor: 'pointer',
275
+ color: 'var(--secondary-color, #aaa)',
276
+ },
277
+ },
278
+ };
279
+
280
+ const pickerBody = (
281
+ <div css={pickerBodyCss}>
282
+ <div class='picker-body'>{orderedCols}</div>
283
+ </div>
284
+ );
285
+
286
+ handleClose = await ActionSheet.show({
287
+ title,
288
+ children: pickerBody,
289
+ contentMaxHeight: '33vh',
290
+ closeEvent,
291
+ closeWhenClickOutside: true,
292
+ zIndex,
293
+ confirmButtonText,
294
+ handleConfirmClicked: onConfirm,
295
+ cancelButtonText,
296
+ buttonsPosition: 'header',
297
+ });
298
+ });
299
+ };