@windrun-huaiin/third-ui 29.2.0 → 30.0.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 (102) hide show
  1. package/dist/fuma/mdx/cheet-table.d.ts +13 -0
  2. package/dist/fuma/mdx/cheet-table.js +295 -0
  3. package/dist/fuma/mdx/cheet-table.mjs +293 -0
  4. package/dist/fuma/mdx/index.d.ts +1 -0
  5. package/dist/fuma/mdx/index.js +2 -0
  6. package/dist/fuma/mdx/index.mjs +1 -0
  7. package/dist/fuma/server/features/widgets.js +2 -0
  8. package/dist/fuma/server/features/widgets.mjs +2 -0
  9. package/dist/lib/fuma-schema-check-util.d.ts +1 -1
  10. package/dist/main/alert-dialog/confirm-dialog.d.ts +2 -1
  11. package/dist/main/alert-dialog/confirm-dialog.js +3 -3
  12. package/dist/main/alert-dialog/confirm-dialog.mjs +4 -4
  13. package/dist/main/alert-dialog/dialog-loading-action.d.ts +2 -1
  14. package/dist/main/alert-dialog/dialog-loading-action.js +6 -3
  15. package/dist/main/alert-dialog/dialog-loading-action.mjs +6 -3
  16. package/dist/main/alert-dialog/dialog-styles.d.ts +4 -2
  17. package/dist/main/alert-dialog/dialog-styles.js +8 -4
  18. package/dist/main/alert-dialog/dialog-styles.mjs +7 -5
  19. package/dist/main/alert-dialog/high-priority-confirm-dialog.d.ts +2 -1
  20. package/dist/main/alert-dialog/high-priority-confirm-dialog.js +7 -7
  21. package/dist/main/alert-dialog/high-priority-confirm-dialog.mjs +8 -8
  22. package/dist/main/alert-dialog/info-dialog.d.ts +2 -1
  23. package/dist/main/alert-dialog/info-dialog.js +3 -3
  24. package/dist/main/alert-dialog/info-dialog.mjs +4 -4
  25. package/dist/main/alert-dialog/undoable-confirm-dialog.d.ts +2 -1
  26. package/dist/main/alert-dialog/undoable-confirm-dialog.js +4 -4
  27. package/dist/main/alert-dialog/undoable-confirm-dialog.mjs +5 -5
  28. package/dist/main/anime/anime-beam-frame.d.ts +3 -0
  29. package/dist/main/anime/anime-beam-frame.js +63 -0
  30. package/dist/main/anime/anime-beam-frame.mjs +61 -0
  31. package/dist/main/anime/anime-spiral-loading.d.ts +10 -0
  32. package/dist/main/anime/anime-spiral-loading.js +77 -0
  33. package/dist/main/anime/anime-spiral-loading.mjs +75 -0
  34. package/dist/main/anime/index.d.ts +2 -0
  35. package/dist/main/anime/index.js +10 -0
  36. package/dist/main/anime/index.mjs +3 -0
  37. package/dist/main/beam-frame/animate.d.ts +3 -0
  38. package/dist/main/beam-frame/animate.js +63 -0
  39. package/dist/main/beam-frame/animate.mjs +61 -0
  40. package/dist/main/beam-frame/beam-frame.d.ts +4 -0
  41. package/dist/main/beam-frame/beam-frame.js +262 -0
  42. package/dist/main/beam-frame/beam-frame.mjs +258 -0
  43. package/dist/main/beam-frame/index.d.ts +4 -0
  44. package/dist/main/beam-frame/index.js +11 -0
  45. package/dist/main/beam-frame/index.mjs +3 -0
  46. package/dist/main/beam-frame/motion.d.ts +3 -0
  47. package/dist/main/beam-frame/motion.js +61 -0
  48. package/dist/main/beam-frame/motion.mjs +59 -0
  49. package/dist/main/beam-frame/share-config.d.ts +54 -0
  50. package/dist/main/beam-frame/share-config.js +161 -0
  51. package/dist/main/beam-frame/share-config.mjs +152 -0
  52. package/dist/main/beam-frame-config.d.ts +54 -0
  53. package/dist/main/beam-frame-config.js +161 -0
  54. package/dist/main/beam-frame-config.mjs +152 -0
  55. package/dist/main/calendar/random-date-range-dialog.d.ts +5 -2
  56. package/dist/main/calendar/random-date-range-dialog.js +239 -109
  57. package/dist/main/calendar/random-date-range-dialog.mjs +242 -112
  58. package/dist/main/cta.js +17 -1
  59. package/dist/main/cta.mjs +18 -2
  60. package/dist/main/delayed-img.d.ts +1 -1
  61. package/dist/main/delayed-img.js +8 -5
  62. package/dist/main/delayed-img.mjs +8 -5
  63. package/dist/main/info-tooltip.js +70 -9
  64. package/dist/main/info-tooltip.mjs +70 -9
  65. package/dist/main/loading-frame/index.d.ts +1 -0
  66. package/dist/main/loading.d.ts +2 -1
  67. package/dist/main/loading.js +64 -26
  68. package/dist/main/loading.mjs +64 -26
  69. package/dist/main/motion/index.d.ts +1 -0
  70. package/dist/main/motion/index.js +9 -0
  71. package/dist/main/motion/index.mjs +2 -0
  72. package/dist/main/motion/motion-beam-frame.d.ts +3 -0
  73. package/dist/main/motion/motion-beam-frame.js +61 -0
  74. package/dist/main/motion/motion-beam-frame.mjs +59 -0
  75. package/dist/main/snake-loading-frame.d.ts +7 -3
  76. package/dist/main/snake-loading-frame.js +44 -252
  77. package/dist/main/snake-loading-frame.mjs +46 -254
  78. package/package.json +16 -5
  79. package/src/fuma/mdx/cheet-table.tsx +650 -0
  80. package/src/fuma/mdx/index.ts +1 -0
  81. package/src/fuma/server/features/widgets.tsx +2 -0
  82. package/src/main/alert-dialog/confirm-dialog.tsx +5 -2
  83. package/src/main/alert-dialog/dialog-loading-action.tsx +22 -5
  84. package/src/main/alert-dialog/dialog-styles.ts +13 -3
  85. package/src/main/alert-dialog/high-priority-confirm-dialog.tsx +29 -24
  86. package/src/main/alert-dialog/info-dialog.tsx +5 -2
  87. package/src/main/alert-dialog/undoable-confirm-dialog.tsx +21 -18
  88. package/src/main/anime/anime-beam-frame.tsx +128 -0
  89. package/src/main/anime/anime-spiral-loading.tsx +123 -0
  90. package/src/main/anime/index.ts +9 -0
  91. package/src/main/beam-frame-config.tsx +341 -0
  92. package/src/main/calendar/random-date-range-dialog.tsx +242 -74
  93. package/src/main/cta.tsx +50 -21
  94. package/src/main/delayed-img.tsx +9 -4
  95. package/src/main/info-tooltip.tsx +116 -20
  96. package/src/main/loading-frame/index.ts +4 -0
  97. package/src/main/loading.tsx +75 -24
  98. package/src/main/motion/index.ts +8 -0
  99. package/src/main/motion/motion-beam-frame.tsx +137 -0
  100. package/src/main/snake-loading-frame.tsx +95 -496
  101. package/src/styles/cta.css +21 -4
  102. package/src/styles/third-ui.css +0 -20
@@ -10,7 +10,10 @@ import {
10
10
  ChevronsRightIcon,
11
11
  XIcon,
12
12
  } from '@windrun-huaiin/base-ui/icons';
13
+ import { themeSvgIconColor } from '@windrun-huaiin/base-ui/lib';
13
14
  import { cn } from '@windrun-huaiin/lib/utils';
15
+ import { DialogLoadingAction, DialogActionHandler, useDialogLoadingAction } from '../alert-dialog/dialog-loading-action';
16
+ import { XButton } from '../buttons/x-button';
14
17
  import { usePressFeedback } from '../buttons/use-press-feedback';
15
18
 
16
19
  export type RandomCalendarRange = {
@@ -24,7 +27,9 @@ type RandomDateRangeDialogProps = {
24
27
  anchorDate: string;
25
28
  defaultRangeDays?: number;
26
29
  onOpenChange: (open: boolean) => void;
27
- onApply: (range: RandomCalendarRange) => void;
30
+ loadingActions?: readonly DialogLoadingAction[];
31
+ loadingFullPage?: boolean;
32
+ onApply: (range: RandomCalendarRange) => void | Promise<void>;
28
33
  onClear?: (range: RandomCalendarRange) => void;
29
34
  };
30
35
 
@@ -32,11 +37,11 @@ type QuickRangeDays = 7 | 10 | 15 | 30;
32
37
  type DialogNavButtonKey = 'prevYear' | 'prevMonth' | 'nextMonth' | 'nextYear';
33
38
 
34
39
  const DEFAULT_RANGE_DAYS = 7;
35
- const MAX_RANGE_DAYS = 30;
36
- const TRACK_MIN_DAYS = 45;
37
- const TRACK_PADDING_DAYS = 20;
40
+ const MAX_RANGE_DAYS = 31;
41
+ const VISIBLE_TRACK_DAYS = 36;
42
+ const EDGE_OVERFLOW_PIXELS_PER_DAY = 24;
38
43
  const DIALOG_ICON_BUTTON_CLASS_NAME =
39
- 'inline-flex h-8 w-8 items-center justify-center rounded-full border border-black/10 bg-white text-slate-500 transition duration-150 hover:border-black/20 hover:bg-black/5 hover:text-slate-900 dark:border-white/10 dark:bg-slate-950 dark:text-slate-300 dark:hover:bg-white/5 dark:hover:text-white';
44
+ 'inline-flex h-8 w-8 items-center justify-center rounded-full bg-white text-slate-500 transition duration-150 hover:bg-black/5 hover:text-slate-900 dark:bg-slate-950 dark:text-slate-300 dark:hover:bg-white/5 dark:hover:text-white';
40
45
  const DIALOG_NAV_BUTTON_CLASS_NAME =
41
46
  'inline-flex h-8 w-8 items-center justify-center rounded-full transition-[transform,background-color,color,box-shadow,border-color] duration-150 ease-out';
42
47
  const DIALOG_NAV_BUTTON_REST_CLASS_NAME =
@@ -95,13 +100,38 @@ function clampWindowDays(days: number): number {
95
100
 
96
101
  function buildTrackRange(referenceDate: string, windowDays = DEFAULT_RANGE_DAYS): RandomCalendarRange {
97
102
  const resolvedWindowDays = clampWindowDays(windowDays);
98
- const resolvedTotalDays = Math.max(TRACK_MIN_DAYS, resolvedWindowDays + TRACK_PADDING_DAYS);
99
- const daysBefore = Math.floor((resolvedTotalDays - resolvedWindowDays) / 3);
103
+ const daysBefore = Math.floor((VISIBLE_TRACK_DAYS - resolvedWindowDays) / 3);
100
104
  const startDate = addDays(referenceDate, -daysBefore);
101
- const endDate = addDays(startDate, resolvedTotalDays - 1);
105
+ const endDate = addDays(startDate, VISIBLE_TRACK_DAYS - 1);
102
106
  return { startDate, endDate };
103
107
  }
104
108
 
109
+ function ensureRangeVisibleOnTrack(range: RandomCalendarRange, bounds: RandomCalendarRange): RandomCalendarRange {
110
+ if (!range.startDate || !range.endDate || !bounds.startDate || !bounds.endDate) {
111
+ return bounds;
112
+ }
113
+
114
+ let nextStartDate = bounds.startDate;
115
+
116
+ if (compareDateStrings(range.startDate, nextStartDate) < 0) {
117
+ nextStartDate = range.startDate;
118
+ }
119
+
120
+ const nextEndDate = addDays(nextStartDate, VISIBLE_TRACK_DAYS - 1);
121
+ if (compareDateStrings(range.endDate, nextEndDate) > 0) {
122
+ nextStartDate = addDays(range.endDate, -(VISIBLE_TRACK_DAYS - 1));
123
+ }
124
+
125
+ if (nextStartDate === bounds.startDate && addDays(nextStartDate, VISIBLE_TRACK_DAYS - 1) === bounds.endDate) {
126
+ return bounds;
127
+ }
128
+
129
+ return {
130
+ startDate: nextStartDate,
131
+ endDate: addDays(nextStartDate, VISIBLE_TRACK_DAYS - 1),
132
+ };
133
+ }
134
+
105
135
  function clampDateToRange(date: string, bounds: RandomCalendarRange): string {
106
136
  if (!bounds.startDate || !bounds.endDate) {
107
137
  return date;
@@ -130,6 +160,23 @@ function getDateByRatio(bounds: RandomCalendarRange, ratio: number): string {
130
160
  }
131
161
 
132
162
  const totalDays = Math.max(1, getDaysBetween(bounds.startDate, bounds.endDate));
163
+ return addDays(bounds.startDate, Math.round(totalDays * Math.max(0, Math.min(1, ratio))));
164
+ }
165
+
166
+ function getDateByOverflowRatio(bounds: RandomCalendarRange, ratio: number, trackWidth: number): string {
167
+ if (!bounds.startDate || !bounds.endDate) {
168
+ return getTodayString();
169
+ }
170
+
171
+ const totalDays = Math.max(1, getDaysBetween(bounds.startDate, bounds.endDate));
172
+ if (ratio < 0) {
173
+ return addDays(bounds.startDate, Math.floor((ratio * trackWidth) / EDGE_OVERFLOW_PIXELS_PER_DAY));
174
+ }
175
+
176
+ if (ratio > 1) {
177
+ return addDays(bounds.endDate, Math.ceil(((ratio - 1) * trackWidth) / EDGE_OVERFLOW_PIXELS_PER_DAY));
178
+ }
179
+
133
180
  return addDays(bounds.startDate, Math.round(totalDays * ratio));
134
181
  }
135
182
 
@@ -174,12 +221,47 @@ function getMonthEnd(value: string): string {
174
221
  return formatDateString(new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + 1, 0)));
175
222
  }
176
223
 
224
+ function RollingMonthLabel({ value }: { value: string }) {
225
+ const [displayValue, setDisplayValue] = useState(value);
226
+ const [previousValue, setPreviousValue] = useState<string | null>(null);
227
+
228
+ useEffect(() => {
229
+ if (value === displayValue) {
230
+ return undefined;
231
+ }
232
+
233
+ setPreviousValue(displayValue);
234
+ setDisplayValue(value);
235
+
236
+ const timeout = window.setTimeout(() => {
237
+ setPreviousValue(null);
238
+ }, 180);
239
+
240
+ return () => window.clearTimeout(timeout);
241
+ }, [displayValue, value]);
242
+
243
+ return (
244
+ <span className="relative inline-block h-5 min-w-10 overflow-hidden align-bottom">
245
+ {previousValue ? (
246
+ <span className="rd-date-range-month-out absolute inset-x-0 top-0 text-center">
247
+ {previousValue}
248
+ </span>
249
+ ) : null}
250
+ <span className={cn('absolute inset-x-0 top-0 text-center', previousValue && 'rd-date-range-month-in')}>
251
+ {displayValue}
252
+ </span>
253
+ </span>
254
+ );
255
+ }
256
+
177
257
  export function RandomDateRangeDialog({
178
258
  open,
179
259
  value,
180
260
  anchorDate,
181
261
  defaultRangeDays = DEFAULT_RANGE_DAYS,
182
262
  onOpenChange,
263
+ loadingActions,
264
+ loadingFullPage,
183
265
  onApply,
184
266
  onClear,
185
267
  }: RandomDateRangeDialogProps) {
@@ -204,12 +286,15 @@ export function RandomDateRangeDialog({
204
286
  const resultLabelRef = useRef<HTMLDivElement | null>(null);
205
287
  const selectionDaysRef = useRef<HTMLDivElement | null>(null);
206
288
  const dragPreviewRef = useRef<RandomCalendarRange | null>(null);
289
+ const trackBoundsRef = useRef<RandomCalendarRange>(trackBounds);
290
+ const dragStartTrackBoundsRef = useRef<RandomCalendarRange | null>(null);
207
291
  const frameRef = useRef<number | null>(null);
208
292
  const pendingClientXRef = useRef<number | null>(null);
209
293
  const syncPreviewDomRef = useRef<(range: RandomCalendarRange) => void>(() => {});
210
294
  const buildDraggedRangeRef = useRef<(clientX: number) => RandomCalendarRange | null>(() => null);
211
295
  const previousBodyOverflowRef = useRef<string | null>(null);
212
296
  const today = useMemo(() => getTodayString(), []);
297
+ const { dialogLoading, runDialogAction } = useDialogLoadingAction({ loadingActions, loadingFullPage, onOpenChange });
213
298
  const baseReferenceDate = anchorDate || today;
214
299
  const previousOpenRef = useRef(false);
215
300
  const startRatio = getRatioByDate(draftRange.startDate ?? baseReferenceDate, trackBounds);
@@ -220,24 +305,30 @@ export function RandomDateRangeDialog({
220
305
  const isSingleDay = (draftRange.startDate ?? null) === (draftRange.endDate ?? null);
221
306
  const startHandlePercent = isSingleDay ? Math.max(leftPercent - 0.8, 0) : leftPercent;
222
307
  const endHandlePercent = isSingleDay ? Math.min(rightPercent + 0.8, 100) : rightPercent;
223
- const trackTickCount = Math.max(getDaysBetween(trackBounds.startDate ?? baseReferenceDate, trackBounds.endDate ?? baseReferenceDate) + 1, 2);
224
- const monthLabels = useMemo(() => {
225
- const values = [trackBounds.startDate, trackBounds.endDate]
226
- .filter((item): item is string => Boolean(item))
227
- .map((item) => formatMonthShort(item));
308
+ const trackTickCount = VISIBLE_TRACK_DAYS;
309
+ const leftMonthLabel = formatMonthShort(trackBounds.startDate ?? baseReferenceDate);
310
+ const rightMonthLabel = formatMonthShort(trackBounds.endDate ?? baseReferenceDate);
228
311
 
229
- return [...new Set(values)];
230
- }, [trackBounds.endDate, trackBounds.startDate]);
312
+ const handleApply = useCallback<DialogActionHandler>(() => {
313
+ return onApply(draftRange);
314
+ }, [draftRange, onApply]);
315
+
316
+ function commitTrackBounds(nextTrackBounds: RandomCalendarRange) {
317
+ trackBoundsRef.current = nextTrackBounds;
318
+ setTrackBounds(nextTrackBounds);
319
+ }
231
320
 
232
321
  useEffect(() => {
233
322
  if (open && !previousOpenRef.current) {
323
+ const nextTrackBounds = buildTrackRange(baseReferenceDate, resolvedDefaultRangeDays);
234
324
  const nextRange = {
235
325
  startDate: baseReferenceDate,
236
326
  endDate: addDays(baseReferenceDate, resolvedDefaultRangeDays - 1),
237
327
  };
238
328
  setDraftRange(nextRange);
239
329
  setReferenceDate(baseReferenceDate);
240
- setTrackBounds(buildTrackRange(baseReferenceDate, resolvedDefaultRangeDays));
330
+ trackBoundsRef.current = nextTrackBounds;
331
+ setTrackBounds(nextTrackBounds);
241
332
  setWindowDays(resolvedDefaultRangeDays);
242
333
  dragStartRangeRef.current = null;
243
334
  dragModeRef.current = null;
@@ -256,16 +347,19 @@ export function RandomDateRangeDialog({
256
347
  setReferenceDate(nextReferenceDate);
257
348
  setWindowDays(clampedWindowDays);
258
349
  setDraftRange(nextRange);
259
- if (!options?.preserveTrack) {
260
- setTrackBounds(buildTrackRange(nextReferenceDate, clampedWindowDays));
350
+ if (options?.preserveTrack) {
351
+ commitTrackBounds(ensureRangeVisibleOnTrack(nextRange, trackBoundsRef.current));
352
+ } else {
353
+ commitTrackBounds(buildTrackRange(nextReferenceDate, clampedWindowDays));
261
354
  }
262
355
  }
263
356
 
264
357
  const getPreviewPercents = useCallback((range: RandomCalendarRange) => {
265
358
  const start = range.startDate ?? baseReferenceDate;
266
359
  const end = range.endDate ?? start;
267
- const startR = getRatioByDate(start, trackBounds);
268
- const endR = getRatioByDate(end, trackBounds);
360
+ const currentTrackBounds = trackBoundsRef.current;
361
+ const startR = getRatioByDate(start, currentTrackBounds);
362
+ const endR = getRatioByDate(end, currentTrackBounds);
269
363
  const left = Math.min(startR, endR) * 100;
270
364
  const right = Math.max(startR, endR) * 100;
271
365
  const width = Math.max(right - left, 0.5);
@@ -278,7 +372,7 @@ export function RandomDateRangeDialog({
278
372
  startHandle: single ? Math.max(left - 0.8, 0) : left,
279
373
  endHandle: single ? Math.min(right + 0.8, 100) : right,
280
374
  };
281
- }, [baseReferenceDate, trackBounds]);
375
+ }, [baseReferenceDate]);
282
376
 
283
377
  const syncPreviewDom = useCallback((range: RandomCalendarRange) => {
284
378
  const percents = getPreviewPercents(range);
@@ -305,19 +399,23 @@ export function RandomDateRangeDialog({
305
399
  syncPreviewDom(draftRange);
306
400
  }, [draftRange, syncPreviewDom]);
307
401
 
402
+ useEffect(() => {
403
+ trackBoundsRef.current = trackBounds;
404
+ }, [trackBounds]);
405
+
308
406
  function resetReferenceFromClientX(clientX: number) {
309
407
  if (!trackRef.current) {
310
408
  return;
311
409
  }
312
410
 
313
411
  const rect = trackRef.current.getBoundingClientRect();
314
- const ratio = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
412
+ const ratio = (clientX - rect.left) / rect.width;
315
413
  const nextReferenceDate = getDateByRatio(trackBounds, ratio);
316
414
  updateRangeByReference(nextReferenceDate, resolvedDefaultRangeDays, { preserveTrack: true });
317
415
  }
318
416
 
319
417
  function applyQuickRange(dayCount: QuickRangeDays) {
320
- updateRangeByReference(referenceDate, dayCount);
418
+ updateRangeByReference(referenceDate, dayCount, { preserveTrack: true });
321
419
  }
322
420
 
323
421
  function shiftReferenceDateByMonths(monthOffset: number) {
@@ -334,6 +432,7 @@ export function RandomDateRangeDialog({
334
432
  pointerIdRef.current = pointerId;
335
433
  dragStartRangeRef.current = { ...draftRange };
336
434
  dragPreviewRef.current = { ...draftRange };
435
+ dragStartTrackBoundsRef.current = { ...trackBoundsRef.current };
337
436
 
338
437
  if (
339
438
  mode === 'window' &&
@@ -341,12 +440,12 @@ export function RandomDateRangeDialog({
341
440
  trackRef.current &&
342
441
  draftRange.startDate &&
343
442
  draftRange.endDate &&
344
- trackBounds.startDate &&
345
- trackBounds.endDate
443
+ dragStartTrackBoundsRef.current.startDate &&
444
+ dragStartTrackBoundsRef.current.endDate
346
445
  ) {
347
446
  const rect = trackRef.current.getBoundingClientRect();
348
- const ratio = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
349
- const pointerDate = getDateByRatio(trackBounds, ratio);
447
+ const ratio = (clientX - rect.left) / rect.width;
448
+ const pointerDate = getDateByRatio(dragStartTrackBoundsRef.current, ratio);
350
449
  windowDragOffsetDaysRef.current = getDaysBetween(draftRange.startDate, pointerDate);
351
450
  } else {
352
451
  windowDragOffsetDaysRef.current = 0;
@@ -354,13 +453,23 @@ export function RandomDateRangeDialog({
354
453
  }
355
454
 
356
455
  const buildDraggedRange = useCallback((clientX: number): RandomCalendarRange | null => {
357
- if (!dragModeRef.current || !dragStartRangeRef.current || !trackBounds.startDate || !trackBounds.endDate || !trackRef.current) {
456
+ const currentTrackBounds = trackBoundsRef.current;
457
+ const dragStartTrackBounds = dragStartTrackBoundsRef.current;
458
+ if (
459
+ !dragModeRef.current ||
460
+ !dragStartRangeRef.current ||
461
+ !dragStartTrackBounds?.startDate ||
462
+ !dragStartTrackBounds.endDate ||
463
+ !currentTrackBounds.startDate ||
464
+ !currentTrackBounds.endDate ||
465
+ !trackRef.current
466
+ ) {
358
467
  return null;
359
468
  }
360
469
 
361
470
  const rect = trackRef.current.getBoundingClientRect();
362
- const ratio = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
363
- const pointerDate = getDateByRatio(trackBounds, ratio);
471
+ const ratio = (clientX - rect.left) / rect.width;
472
+ const pointerDate = getDateByOverflowRatio(dragStartTrackBounds, ratio, rect.width);
364
473
  const currentRange = dragStartRangeRef.current;
365
474
 
366
475
  if (!currentRange.startDate || !currentRange.endDate) {
@@ -371,24 +480,42 @@ export function RandomDateRangeDialog({
371
480
  const earliestStart = addDays(currentRange.endDate, -(MAX_RANGE_DAYS - 1));
372
481
  const boundedPointerDate = compareDateStrings(pointerDate, earliestStart) < 0 ? earliestStart : pointerDate;
373
482
  const nextStart = compareDateStrings(boundedPointerDate, currentRange.endDate) > 0 ? currentRange.endDate : boundedPointerDate;
374
- return { startDate: nextStart, endDate: currentRange.endDate };
483
+ const nextRange = { startDate: nextStart, endDate: currentRange.endDate };
484
+ const nextTrackBounds = ensureRangeVisibleOnTrack(nextRange, currentTrackBounds);
485
+ if (nextTrackBounds !== currentTrackBounds) {
486
+ trackBoundsRef.current = nextTrackBounds;
487
+ setDraftRange(nextRange);
488
+ setTrackBounds(nextTrackBounds);
489
+ }
490
+ return nextRange;
375
491
  }
376
492
 
377
493
  if (dragModeRef.current === 'end') {
378
494
  const latestEnd = addDays(currentRange.startDate, MAX_RANGE_DAYS - 1);
379
495
  const boundedPointerDate = compareDateStrings(pointerDate, latestEnd) > 0 ? latestEnd : pointerDate;
380
496
  const nextEnd = compareDateStrings(boundedPointerDate, currentRange.startDate) < 0 ? currentRange.startDate : boundedPointerDate;
381
- return { startDate: currentRange.startDate, endDate: nextEnd };
497
+ const nextRange = { startDate: currentRange.startDate, endDate: nextEnd };
498
+ const nextTrackBounds = ensureRangeVisibleOnTrack(nextRange, currentTrackBounds);
499
+ if (nextTrackBounds !== currentTrackBounds) {
500
+ trackBoundsRef.current = nextTrackBounds;
501
+ setDraftRange(nextRange);
502
+ setTrackBounds(nextTrackBounds);
503
+ }
504
+ return nextRange;
382
505
  }
383
506
 
384
507
  const spanDays = getDaysBetween(currentRange.startDate, currentRange.endDate);
385
- const nextStart = clampDateToRange(addDays(pointerDate, -windowDragOffsetDaysRef.current), {
386
- startDate: trackBounds.startDate,
387
- endDate: addDays(trackBounds.endDate, -spanDays),
388
- });
508
+ const nextStart = addDays(pointerDate, -windowDragOffsetDaysRef.current);
389
509
  const nextEnd = addDays(nextStart, spanDays);
390
- return { startDate: nextStart, endDate: nextEnd };
391
- }, [trackBounds]);
510
+ const nextRange = { startDate: nextStart, endDate: nextEnd };
511
+ const nextTrackBounds = ensureRangeVisibleOnTrack(nextRange, currentTrackBounds);
512
+ if (nextTrackBounds !== currentTrackBounds) {
513
+ trackBoundsRef.current = nextTrackBounds;
514
+ setDraftRange(nextRange);
515
+ setTrackBounds(nextTrackBounds);
516
+ }
517
+ return nextRange;
518
+ }, []);
392
519
 
393
520
  useEffect(() => {
394
521
  syncPreviewDomRef.current = syncPreviewDom;
@@ -405,6 +532,7 @@ export function RandomDateRangeDialog({
405
532
 
406
533
  const nextRange = dragPreviewRef.current;
407
534
  dragStartRangeRef.current = null;
535
+ dragStartTrackBoundsRef.current = null;
408
536
  dragModeRef.current = null;
409
537
  pointerIdRef.current = null;
410
538
  windowDragOffsetDaysRef.current = 0;
@@ -412,6 +540,7 @@ export function RandomDateRangeDialog({
412
540
  setDraftRange(nextRange);
413
541
  setReferenceDate(nextRange.startDate);
414
542
  setWindowDays(getInclusiveDayCount(nextRange));
543
+ commitTrackBounds(ensureRangeVisibleOnTrack(nextRange, trackBoundsRef.current));
415
544
  }
416
545
  }
417
546
 
@@ -472,16 +601,17 @@ export function RandomDateRangeDialog({
472
601
  }, [open]);
473
602
 
474
603
  if (!open) {
475
- return null;
604
+ return <>{dialogLoading}</>;
476
605
  }
477
606
 
478
607
  return createPortal(
479
- <div className="fixed inset-0 z-120 flex select-none items-center justify-center bg-slate-950/60 px-3 py-6 backdrop-blur-sm">
480
- <div className="w-full max-w-2xl overflow-hidden rounded-3xl border border-black/10 bg-white shadow-2xl dark:border-white/10 dark:bg-slate-950">
608
+ <>
609
+ <div className="fixed inset-0 z-120 flex select-none items-center justify-center bg-slate-950/60 px-3 py-6 backdrop-blur-sm">
610
+ <div className="w-full max-w-2xl overflow-hidden rounded-3xl border border-black/10 bg-white shadow-2xl dark:border-white/10 dark:bg-slate-950">
481
611
  <div className="space-y-5 p-4">
482
- <div className="relative flex items-center justify-center px-16 text-center">
483
- <div ref={resultLabelRef} className="select-none text-base font-semibold text-slate-900 dark:text-white">{getRangeLabel(draftRange)}</div>
484
- <div className="absolute right-0 top-1/2 flex -translate-y-1/2 items-center gap-2">
612
+ <div className="relative flex items-center justify-center px-9 text-center sm:px-16">
613
+ <div ref={resultLabelRef} className="min-w-0 select-none truncate text-base font-semibold text-slate-900 dark:text-white">{getRangeLabel(draftRange)}</div>
614
+ <div className="absolute right-0 top-1/2 flex -translate-y-1/2 translate-x-1 items-center sm:translate-x-0">
485
615
  <button
486
616
  type="button"
487
617
  onClick={() => onOpenChange(false)}
@@ -490,26 +620,10 @@ export function RandomDateRangeDialog({
490
620
  >
491
621
  <XIcon className="h-4 w-4" />
492
622
  </button>
493
- <button
494
- type="button"
495
- onClick={() => {
496
- onApply(draftRange);
497
- onOpenChange(false);
498
- }}
499
- disabled={!draftRange.startDate || !draftRange.endDate}
500
- className={cn(
501
- DIALOG_ICON_BUTTON_CLASS_NAME,
502
- 'text-slate-700 dark:text-slate-100',
503
- 'disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:border-black/10 disabled:hover:bg-white disabled:hover:text-slate-700 dark:disabled:hover:border-white/10 dark:disabled:hover:bg-slate-950 dark:disabled:hover:text-slate-100'
504
- )}
505
- aria-label="Apply"
506
- >
507
- <CheckCheckIcon className="h-4 w-4" />
508
- </button>
509
623
  </div>
510
624
  </div>
511
625
 
512
- <div className="space-y-4">
626
+ <div className="space-y-3">
513
627
  <div className="flex items-center justify-between gap-2">
514
628
  <div className="flex items-center gap-1">
515
629
  <button
@@ -559,14 +673,14 @@ export function RandomDateRangeDialog({
559
673
  endDate: addDays(baseReferenceDate, resolvedDefaultRangeDays - 1),
560
674
  };
561
675
  setReferenceDate(baseReferenceDate);
562
- setTrackBounds(buildTrackRange(baseReferenceDate, resolvedDefaultRangeDays));
676
+ commitTrackBounds(buildTrackRange(baseReferenceDate, resolvedDefaultRangeDays));
563
677
  setWindowDays(resolvedDefaultRangeDays);
564
678
  setDraftRange(nextRange);
565
679
  onClear?.(nextRange);
566
680
  }}
567
681
  className={DIALOG_PILL_BUTTON_CLASS_NAME}
568
682
  >
569
- Current Day
683
+ Today
570
684
  </button>
571
685
  <button
572
686
  type="button"
@@ -584,7 +698,8 @@ export function RandomDateRangeDialog({
584
698
  };
585
699
  setDraftRange(normalizedRange);
586
700
  setWindowDays(getInclusiveDayCount(normalizedRange));
587
- setTrackBounds(buildTrackRange(normalizedRange.startDate, getInclusiveDayCount(normalizedRange)));
701
+ setReferenceDate(normalizedRange.startDate);
702
+ commitTrackBounds(buildTrackRange(normalizedRange.startDate, getInclusiveDayCount(normalizedRange)));
588
703
  }}
589
704
  className={DIALOG_PILL_BUTTON_CLASS_NAME}
590
705
  >
@@ -632,10 +747,10 @@ export function RandomDateRangeDialog({
632
747
  </div>
633
748
  </div>
634
749
 
635
- <div className="relative h-24">
750
+ <div className="relative h-21">
636
751
  <div className="absolute inset-x-0 top-0 grid grid-cols-[3.5rem_minmax(0,1fr)_3.5rem] items-center gap-2 text-sm font-semibold text-slate-500 dark:text-slate-400">
637
752
  <span className="relative block select-none text-center">
638
- {monthLabels[0] ?? formatMonthShort(trackBounds.startDate ?? baseReferenceDate)}
753
+ <RollingMonthLabel value={leftMonthLabel} />
639
754
  <span className="pointer-events-none absolute left-1/2 top-7 h-2.5 w-2.5 -translate-x-1/2 rounded-full bg-slate-400 dark:bg-slate-500" />
640
755
  <span className="pointer-events-none absolute left-1/2 top-[1.95rem] h-9 w-0.5 -translate-x-1/2 bg-slate-400 dark:bg-slate-500" />
641
756
  </span>
@@ -657,7 +772,7 @@ export function RandomDateRangeDialog({
657
772
  ))}
658
773
  </div>
659
774
  <span className="relative block select-none text-center">
660
- {monthLabels[1] ?? formatMonthShort(trackBounds.endDate ?? baseReferenceDate)}
775
+ <RollingMonthLabel value={rightMonthLabel} />
661
776
  <span className="pointer-events-none absolute right-1/2 top-7 h-2.5 w-2.5 translate-x-1/2 rounded-full bg-slate-400 dark:bg-slate-500" />
662
777
  <span className="pointer-events-none absolute right-1/2 top-[1.95rem] h-9 w-0.5 translate-x-1/2 bg-slate-400 dark:bg-slate-500" />
663
778
  </span>
@@ -694,8 +809,8 @@ export function RandomDateRangeDialog({
694
809
  </div>
695
810
  <div
696
811
  ref={selectionRef}
697
- className="absolute top-1/2 z-10 h-4 touch-none -translate-y-1/2 overflow-visible rounded-md border border-sky-500 bg-white dark:border-sky-300 dark:bg-slate-950"
698
- style={{ left: `${leftPercent}%`, width: `${widthPercent}%` }}
812
+ className="absolute top-1/2 z-10 h-4 touch-none -translate-y-1/2 overflow-visible rounded-md border bg-white dark:bg-slate-950"
813
+ style={{ left: `${leftPercent}%`, width: `${widthPercent}%`, borderColor: themeSvgIconColor }}
699
814
  onPointerDown={(event) => {
700
815
  event.stopPropagation();
701
816
  beginDrag('window', event.pointerId, event.clientX);
@@ -709,8 +824,8 @@ export function RandomDateRangeDialog({
709
824
  <button
710
825
  ref={startHandleRef}
711
826
  type="button"
712
- className="absolute top-1/2 z-20 h-6 w-6 touch-none -translate-x-1/2 -translate-y-1/2 rounded-full border border-sky-500 bg-white shadow-sm dark:border-sky-300 dark:bg-slate-950"
713
- style={{ left: `${startHandlePercent}%` }}
827
+ className="absolute top-1/2 z-20 h-6 w-6 touch-none -translate-x-1/2 -translate-y-1/2 rounded-full border bg-white shadow-sm dark:bg-slate-950"
828
+ style={{ left: `${startHandlePercent}%`, borderColor: themeSvgIconColor }}
714
829
  onPointerDown={(event) => {
715
830
  event.stopPropagation();
716
831
  beginDrag('start', event.pointerId);
@@ -720,8 +835,8 @@ export function RandomDateRangeDialog({
720
835
  <button
721
836
  ref={endHandleRef}
722
837
  type="button"
723
- className="absolute top-1/2 z-20 h-6 w-6 touch-none -translate-x-1/2 -translate-y-1/2 rounded-full border border-sky-500 bg-white shadow-sm dark:border-sky-300 dark:bg-slate-950"
724
- style={{ left: `${endHandlePercent}%` }}
838
+ className="absolute top-1/2 z-20 h-6 w-6 touch-none -translate-x-1/2 -translate-y-1/2 rounded-full border bg-white shadow-sm dark:bg-slate-950"
839
+ style={{ left: `${endHandlePercent}%`, borderColor: themeSvgIconColor }}
725
840
  onPointerDown={(event) => {
726
841
  event.stopPropagation();
727
842
  beginDrag('end', event.pointerId);
@@ -732,10 +847,63 @@ export function RandomDateRangeDialog({
732
847
  </div>
733
848
  </div>
734
849
 
850
+ <div className="flex justify-end">
851
+ <XButton
852
+ type="single"
853
+ variant="soft"
854
+ minWidth="min-w-[110px]"
855
+ className="w-auto"
856
+ iconClassName="h-4 w-4"
857
+ button={{
858
+ icon: <CheckCheckIcon />,
859
+ text: 'Apply',
860
+ disabled: !draftRange.startDate || !draftRange.endDate,
861
+ onClick: () => {
862
+ void runDialogAction('confirm', handleApply);
863
+ },
864
+ }}
865
+ />
866
+ </div>
867
+
735
868
  </div>
736
869
  </div>
870
+ </div>
737
871
  </div>
738
- </div>,
872
+ <style>
873
+ {`
874
+ @keyframes rd-date-range-month-in {
875
+ from {
876
+ opacity: 0;
877
+ transform: translateY(-0.45rem);
878
+ }
879
+ to {
880
+ opacity: 1;
881
+ transform: translateY(0);
882
+ }
883
+ }
884
+
885
+ @keyframes rd-date-range-month-out {
886
+ from {
887
+ opacity: 1;
888
+ transform: translateY(0);
889
+ }
890
+ to {
891
+ opacity: 0;
892
+ transform: translateY(0.45rem);
893
+ }
894
+ }
895
+
896
+ .rd-date-range-month-in {
897
+ animation: rd-date-range-month-in 180ms ease-out both;
898
+ }
899
+
900
+ .rd-date-range-month-out {
901
+ animation: rd-date-range-month-out 180ms ease-out both;
902
+ }
903
+ `}
904
+ </style>
905
+ {dialogLoading}
906
+ </>,
739
907
  document.body
740
908
  );
741
909
  }
package/src/main/cta.tsx CHANGED
@@ -1,9 +1,10 @@
1
1
  import { getTranslations } from 'next-intl/server';
2
2
  import { GradientButton } from "./buttons";
3
3
  import { cn } from '@windrun-huaiin/lib/utils';
4
- import { themeIconColor } from '@windrun-huaiin/base-ui/lib';
4
+ import { themeIconColor, themeName, themeSvgIconColor } from '@windrun-huaiin/base-ui/lib';
5
5
  import { richText } from './rich-text-expert';
6
6
  import { responsiveSection } from './section-layout';
7
+ import type { CSSProperties } from 'react';
7
8
 
8
9
  interface CTAData {
9
10
  title: string;
@@ -14,6 +15,29 @@ interface CTAData {
14
15
  url: string;
15
16
  }
16
17
 
18
+ type CTAThemePalette = {
19
+ b: string;
20
+ c: string;
21
+ };
22
+
23
+ const CTA_THEME_PALETTES: Record<string, CTAThemePalette> = {
24
+ purple: { b: '#EC4899', c: '#6366F1' },
25
+ orange: { b: '#F59E0B', c: '#EF4444' },
26
+ indigo: { b: '#3B82F6', c: '#06B6D4' },
27
+ emerald: { b: '#14B8A6', c: '#22C55E' },
28
+ rose: { b: '#EC4899', c: '#FB7185' },
29
+ };
30
+
31
+ function createCTAStyle(): CSSProperties {
32
+ const palette = CTA_THEME_PALETTES[themeName] ?? CTA_THEME_PALETTES.purple;
33
+
34
+ return {
35
+ '--cta-color-a': themeSvgIconColor,
36
+ '--cta-color-b': palette.b,
37
+ '--cta-color-c': palette.c,
38
+ } as CSSProperties;
39
+ }
40
+
17
41
  export async function CTA({
18
42
  locale,
19
43
  sectionClassName
@@ -34,26 +58,31 @@ export async function CTA({
34
58
 
35
59
  return (
36
60
  <section id="cta" className={cn(responsiveSection, sectionClassName)}>
37
- <div className="
38
- py-3 sm:py-6 md:8
39
- bg-linear-to-r from-[#f7f8fa] via-[#e0c3fc] to-[#b2fefa]
40
- dark:bg-linear-to-r dark:from-[#2d0b4e] dark:via-[#6a3fa0] dark:to-[#3a185a]
41
- rounded-2xl text-center
42
- bg-size[200%_auto] animate-cta-gradient-wave
43
- ">
44
- <h2 className="text-3xl md:text-4xl font-bold mb-6">
45
- {data.title} <span className={themeIconColor}>{data.eyesOn}</span>?
46
- </h2>
47
- <p className="text-base sm:text-xl mx-auto mb-8 max-w-3xl">
48
- {data.description1}
49
- <br />
50
- <span className={cn(themeIconColor, "text-xl sm:text-2xl")}>{data.description2}</span>
51
- </p>
52
- <GradientButton
53
- title={data.button}
54
- href={data.url}
55
- align="center"
56
- />
61
+ <div
62
+ className="
63
+ third-ui-cta-surface
64
+ relative overflow-hidden rounded-2xl border border-black/5 py-3 text-center shadow-sm
65
+ animate-cta-gradient-wave
66
+ sm:py-6 md:py-8
67
+ dark:border-white/10 dark:shadow-none
68
+ "
69
+ style={createCTAStyle()}
70
+ >
71
+ <div className="relative z-10 px-4 sm:px-6">
72
+ <h2 className="mb-6 text-3xl font-bold text-neutral-950 md:text-4xl dark:text-neutral-50">
73
+ {data.title} <span className={themeIconColor}>{data.eyesOn}</span>?
74
+ </h2>
75
+ <p className="mx-auto mb-8 max-w-3xl text-base text-neutral-700 sm:text-xl dark:text-neutral-300">
76
+ {data.description1}
77
+ <br />
78
+ <span className={cn(themeIconColor, "text-xl sm:text-2xl")}>{data.description2}</span>
79
+ </p>
80
+ <GradientButton
81
+ title={data.button}
82
+ href={data.url}
83
+ align="center"
84
+ />
85
+ </div>
57
86
  </div>
58
87
  </section>
59
88
  )