@windrun-huaiin/third-ui 29.2.1 → 30.1.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 (121) hide show
  1. package/dist/fuma/fuma-page-genarator.d.ts +2 -6
  2. package/dist/fuma/fuma-page-genarator.js +3 -2
  3. package/dist/fuma/fuma-page-genarator.mjs +3 -2
  4. package/dist/fuma/mdx/cheet-table.d.ts +13 -0
  5. package/dist/fuma/mdx/cheet-table.js +295 -0
  6. package/dist/fuma/mdx/cheet-table.mjs +293 -0
  7. package/dist/fuma/mdx/index.d.ts +1 -0
  8. package/dist/fuma/mdx/index.js +2 -0
  9. package/dist/fuma/mdx/index.mjs +1 -0
  10. package/dist/fuma/server/features/widgets.js +2 -0
  11. package/dist/fuma/server/features/widgets.mjs +2 -0
  12. package/dist/lib/fuma-schema-check-util.d.ts +1 -1
  13. package/dist/main/404-page.d.ts +12 -0
  14. package/dist/main/404-page.js +66 -0
  15. package/dist/main/404-page.mjs +64 -0
  16. package/dist/main/alert-dialog/confirm-dialog.js +1 -1
  17. package/dist/main/alert-dialog/confirm-dialog.mjs +2 -2
  18. package/dist/main/alert-dialog/dialog-loading-action.js +5 -2
  19. package/dist/main/alert-dialog/dialog-loading-action.mjs +5 -2
  20. package/dist/main/alert-dialog/dialog-styles.d.ts +4 -2
  21. package/dist/main/alert-dialog/dialog-styles.js +8 -4
  22. package/dist/main/alert-dialog/dialog-styles.mjs +7 -5
  23. package/dist/main/alert-dialog/high-priority-confirm-dialog.js +5 -5
  24. package/dist/main/alert-dialog/high-priority-confirm-dialog.mjs +6 -6
  25. package/dist/main/alert-dialog/info-dialog.js +1 -1
  26. package/dist/main/alert-dialog/info-dialog.mjs +2 -2
  27. package/dist/main/alert-dialog/undoable-confirm-dialog.js +2 -2
  28. package/dist/main/alert-dialog/undoable-confirm-dialog.mjs +3 -3
  29. package/dist/main/anime/anime-404-page.d.ts +14 -0
  30. package/dist/main/anime/anime-404-page.js +197 -0
  31. package/dist/main/anime/anime-404-page.mjs +195 -0
  32. package/dist/main/anime/anime-beam-frame.d.ts +3 -0
  33. package/dist/main/anime/anime-beam-frame.js +63 -0
  34. package/dist/main/anime/anime-beam-frame.mjs +61 -0
  35. package/dist/main/anime/anime-not-found-page.d.ts +7 -0
  36. package/dist/main/anime/anime-not-found-page.js +142 -0
  37. package/dist/main/anime/anime-not-found-page.mjs +140 -0
  38. package/dist/main/anime/anime-spiral-loading.d.ts +10 -0
  39. package/dist/main/anime/anime-spiral-loading.js +77 -0
  40. package/dist/main/anime/anime-spiral-loading.mjs +75 -0
  41. package/dist/main/anime/index.d.ts +3 -0
  42. package/dist/main/anime/index.js +12 -0
  43. package/dist/main/anime/index.mjs +4 -0
  44. package/dist/main/beam-frame/animate.d.ts +3 -0
  45. package/dist/main/beam-frame/animate.js +63 -0
  46. package/dist/main/beam-frame/animate.mjs +61 -0
  47. package/dist/main/beam-frame/beam-frame.d.ts +4 -0
  48. package/dist/main/beam-frame/beam-frame.js +262 -0
  49. package/dist/main/beam-frame/beam-frame.mjs +258 -0
  50. package/dist/main/beam-frame/index.d.ts +4 -0
  51. package/dist/main/beam-frame/index.js +11 -0
  52. package/dist/main/beam-frame/index.mjs +3 -0
  53. package/dist/main/beam-frame/motion.d.ts +3 -0
  54. package/dist/main/beam-frame/motion.js +61 -0
  55. package/dist/main/beam-frame/motion.mjs +59 -0
  56. package/dist/main/beam-frame/share-config.d.ts +54 -0
  57. package/dist/main/beam-frame/share-config.js +161 -0
  58. package/dist/main/beam-frame/share-config.mjs +152 -0
  59. package/dist/main/beam-frame-config.d.ts +54 -0
  60. package/dist/main/beam-frame-config.js +161 -0
  61. package/dist/main/beam-frame-config.mjs +152 -0
  62. package/dist/main/calendar/random-date-range-dialog.js +177 -51
  63. package/dist/main/calendar/random-date-range-dialog.mjs +178 -52
  64. package/dist/main/cta.js +17 -1
  65. package/dist/main/cta.mjs +18 -2
  66. package/dist/main/delayed-img.d.ts +1 -1
  67. package/dist/main/delayed-img.js +8 -5
  68. package/dist/main/delayed-img.mjs +8 -5
  69. package/dist/main/index.d.ts +1 -0
  70. package/dist/main/index.js +2 -0
  71. package/dist/main/index.mjs +1 -0
  72. package/dist/main/info-tooltip.js +70 -9
  73. package/dist/main/info-tooltip.mjs +70 -9
  74. package/dist/main/loading-frame/index.d.ts +1 -0
  75. package/dist/main/loading.d.ts +2 -1
  76. package/dist/main/loading.js +64 -26
  77. package/dist/main/loading.mjs +64 -26
  78. package/dist/main/motion/creative-left-panel.d.ts +7 -0
  79. package/dist/main/motion/creative-left-panel.js +11 -0
  80. package/dist/main/motion/creative-left-panel.mjs +9 -0
  81. package/dist/main/motion/creative-right-panel.d.ts +7 -0
  82. package/dist/main/motion/creative-right-panel.js +11 -0
  83. package/dist/main/motion/creative-right-panel.mjs +9 -0
  84. package/dist/main/motion/index.d.ts +1 -0
  85. package/dist/main/motion/index.js +9 -0
  86. package/dist/main/motion/index.mjs +2 -0
  87. package/dist/main/motion/motion-beam-frame.d.ts +3 -0
  88. package/dist/main/motion/motion-beam-frame.js +61 -0
  89. package/dist/main/motion/motion-beam-frame.mjs +59 -0
  90. package/dist/main/snake-loading-frame.d.ts +7 -3
  91. package/dist/main/snake-loading-frame.js +45 -252
  92. package/dist/main/snake-loading-frame.mjs +47 -254
  93. package/package.json +16 -5
  94. package/src/fuma/fuma-page-genarator.tsx +2 -22
  95. package/src/fuma/mdx/cheet-table.tsx +650 -0
  96. package/src/fuma/mdx/index.ts +1 -0
  97. package/src/fuma/server/features/widgets.tsx +2 -0
  98. package/src/main/404-page.tsx +162 -0
  99. package/src/main/alert-dialog/confirm-dialog.tsx +2 -1
  100. package/src/main/alert-dialog/dialog-loading-action.tsx +7 -5
  101. package/src/main/alert-dialog/dialog-styles.ts +13 -3
  102. package/src/main/alert-dialog/high-priority-confirm-dialog.tsx +26 -23
  103. package/src/main/alert-dialog/info-dialog.tsx +2 -1
  104. package/src/main/alert-dialog/undoable-confirm-dialog.tsx +18 -17
  105. package/src/main/anime/anime-404-page.tsx +344 -0
  106. package/src/main/anime/anime-beam-frame.tsx +128 -0
  107. package/src/main/anime/anime-spiral-loading.tsx +123 -0
  108. package/src/main/anime/index.ts +10 -0
  109. package/src/main/beam-frame-config.tsx +341 -0
  110. package/src/main/calendar/random-date-range-dialog.tsx +225 -69
  111. package/src/main/cta.tsx +50 -21
  112. package/src/main/delayed-img.tsx +9 -4
  113. package/src/main/index.ts +1 -0
  114. package/src/main/info-tooltip.tsx +116 -20
  115. package/src/main/loading-frame/index.ts +4 -0
  116. package/src/main/loading.tsx +75 -24
  117. package/src/main/motion/index.ts +8 -0
  118. package/src/main/motion/motion-beam-frame.tsx +137 -0
  119. package/src/main/snake-loading-frame.tsx +95 -496
  120. package/src/styles/cta.css +21 -4
  121. package/src/styles/third-ui.css +0 -20
@@ -10,8 +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';
14
15
  import { DialogLoadingAction, DialogActionHandler, useDialogLoadingAction } from '../alert-dialog/dialog-loading-action';
16
+ import { XButton } from '../buttons/x-button';
15
17
  import { usePressFeedback } from '../buttons/use-press-feedback';
16
18
 
17
19
  export type RandomCalendarRange = {
@@ -35,11 +37,11 @@ type QuickRangeDays = 7 | 10 | 15 | 30;
35
37
  type DialogNavButtonKey = 'prevYear' | 'prevMonth' | 'nextMonth' | 'nextYear';
36
38
 
37
39
  const DEFAULT_RANGE_DAYS = 7;
38
- const MAX_RANGE_DAYS = 30;
39
- const TRACK_MIN_DAYS = 45;
40
- 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;
41
43
  const DIALOG_ICON_BUTTON_CLASS_NAME =
42
- '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';
43
45
  const DIALOG_NAV_BUTTON_CLASS_NAME =
44
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';
45
47
  const DIALOG_NAV_BUTTON_REST_CLASS_NAME =
@@ -98,13 +100,38 @@ function clampWindowDays(days: number): number {
98
100
 
99
101
  function buildTrackRange(referenceDate: string, windowDays = DEFAULT_RANGE_DAYS): RandomCalendarRange {
100
102
  const resolvedWindowDays = clampWindowDays(windowDays);
101
- const resolvedTotalDays = Math.max(TRACK_MIN_DAYS, resolvedWindowDays + TRACK_PADDING_DAYS);
102
- const daysBefore = Math.floor((resolvedTotalDays - resolvedWindowDays) / 3);
103
+ const daysBefore = Math.floor((VISIBLE_TRACK_DAYS - resolvedWindowDays) / 3);
103
104
  const startDate = addDays(referenceDate, -daysBefore);
104
- const endDate = addDays(startDate, resolvedTotalDays - 1);
105
+ const endDate = addDays(startDate, VISIBLE_TRACK_DAYS - 1);
105
106
  return { startDate, endDate };
106
107
  }
107
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
+
108
135
  function clampDateToRange(date: string, bounds: RandomCalendarRange): string {
109
136
  if (!bounds.startDate || !bounds.endDate) {
110
137
  return date;
@@ -133,6 +160,23 @@ function getDateByRatio(bounds: RandomCalendarRange, ratio: number): string {
133
160
  }
134
161
 
135
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
+
136
180
  return addDays(bounds.startDate, Math.round(totalDays * ratio));
137
181
  }
138
182
 
@@ -177,6 +221,39 @@ function getMonthEnd(value: string): string {
177
221
  return formatDateString(new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + 1, 0)));
178
222
  }
179
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
+
180
257
  export function RandomDateRangeDialog({
181
258
  open,
182
259
  value,
@@ -209,6 +286,8 @@ export function RandomDateRangeDialog({
209
286
  const resultLabelRef = useRef<HTMLDivElement | null>(null);
210
287
  const selectionDaysRef = useRef<HTMLDivElement | null>(null);
211
288
  const dragPreviewRef = useRef<RandomCalendarRange | null>(null);
289
+ const trackBoundsRef = useRef<RandomCalendarRange>(trackBounds);
290
+ const dragStartTrackBoundsRef = useRef<RandomCalendarRange | null>(null);
212
291
  const frameRef = useRef<number | null>(null);
213
292
  const pendingClientXRef = useRef<number | null>(null);
214
293
  const syncPreviewDomRef = useRef<(range: RandomCalendarRange) => void>(() => {});
@@ -226,28 +305,30 @@ export function RandomDateRangeDialog({
226
305
  const isSingleDay = (draftRange.startDate ?? null) === (draftRange.endDate ?? null);
227
306
  const startHandlePercent = isSingleDay ? Math.max(leftPercent - 0.8, 0) : leftPercent;
228
307
  const endHandlePercent = isSingleDay ? Math.min(rightPercent + 0.8, 100) : rightPercent;
229
- const trackTickCount = Math.max(getDaysBetween(trackBounds.startDate ?? baseReferenceDate, trackBounds.endDate ?? baseReferenceDate) + 1, 2);
230
- const monthLabels = useMemo(() => {
231
- const values = [trackBounds.startDate, trackBounds.endDate]
232
- .filter((item): item is string => Boolean(item))
233
- .map((item) => formatMonthShort(item));
234
-
235
- return [...new Set(values)];
236
- }, [trackBounds.endDate, trackBounds.startDate]);
308
+ const trackTickCount = VISIBLE_TRACK_DAYS;
309
+ const leftMonthLabel = formatMonthShort(trackBounds.startDate ?? baseReferenceDate);
310
+ const rightMonthLabel = formatMonthShort(trackBounds.endDate ?? baseReferenceDate);
237
311
 
238
312
  const handleApply = useCallback<DialogActionHandler>(() => {
239
313
  return onApply(draftRange);
240
314
  }, [draftRange, onApply]);
241
315
 
316
+ function commitTrackBounds(nextTrackBounds: RandomCalendarRange) {
317
+ trackBoundsRef.current = nextTrackBounds;
318
+ setTrackBounds(nextTrackBounds);
319
+ }
320
+
242
321
  useEffect(() => {
243
322
  if (open && !previousOpenRef.current) {
323
+ const nextTrackBounds = buildTrackRange(baseReferenceDate, resolvedDefaultRangeDays);
244
324
  const nextRange = {
245
325
  startDate: baseReferenceDate,
246
326
  endDate: addDays(baseReferenceDate, resolvedDefaultRangeDays - 1),
247
327
  };
248
328
  setDraftRange(nextRange);
249
329
  setReferenceDate(baseReferenceDate);
250
- setTrackBounds(buildTrackRange(baseReferenceDate, resolvedDefaultRangeDays));
330
+ trackBoundsRef.current = nextTrackBounds;
331
+ setTrackBounds(nextTrackBounds);
251
332
  setWindowDays(resolvedDefaultRangeDays);
252
333
  dragStartRangeRef.current = null;
253
334
  dragModeRef.current = null;
@@ -266,16 +347,19 @@ export function RandomDateRangeDialog({
266
347
  setReferenceDate(nextReferenceDate);
267
348
  setWindowDays(clampedWindowDays);
268
349
  setDraftRange(nextRange);
269
- if (!options?.preserveTrack) {
270
- setTrackBounds(buildTrackRange(nextReferenceDate, clampedWindowDays));
350
+ if (options?.preserveTrack) {
351
+ commitTrackBounds(ensureRangeVisibleOnTrack(nextRange, trackBoundsRef.current));
352
+ } else {
353
+ commitTrackBounds(buildTrackRange(nextReferenceDate, clampedWindowDays));
271
354
  }
272
355
  }
273
356
 
274
357
  const getPreviewPercents = useCallback((range: RandomCalendarRange) => {
275
358
  const start = range.startDate ?? baseReferenceDate;
276
359
  const end = range.endDate ?? start;
277
- const startR = getRatioByDate(start, trackBounds);
278
- const endR = getRatioByDate(end, trackBounds);
360
+ const currentTrackBounds = trackBoundsRef.current;
361
+ const startR = getRatioByDate(start, currentTrackBounds);
362
+ const endR = getRatioByDate(end, currentTrackBounds);
279
363
  const left = Math.min(startR, endR) * 100;
280
364
  const right = Math.max(startR, endR) * 100;
281
365
  const width = Math.max(right - left, 0.5);
@@ -288,7 +372,7 @@ export function RandomDateRangeDialog({
288
372
  startHandle: single ? Math.max(left - 0.8, 0) : left,
289
373
  endHandle: single ? Math.min(right + 0.8, 100) : right,
290
374
  };
291
- }, [baseReferenceDate, trackBounds]);
375
+ }, [baseReferenceDate]);
292
376
 
293
377
  const syncPreviewDom = useCallback((range: RandomCalendarRange) => {
294
378
  const percents = getPreviewPercents(range);
@@ -315,19 +399,23 @@ export function RandomDateRangeDialog({
315
399
  syncPreviewDom(draftRange);
316
400
  }, [draftRange, syncPreviewDom]);
317
401
 
402
+ useEffect(() => {
403
+ trackBoundsRef.current = trackBounds;
404
+ }, [trackBounds]);
405
+
318
406
  function resetReferenceFromClientX(clientX: number) {
319
407
  if (!trackRef.current) {
320
408
  return;
321
409
  }
322
410
 
323
411
  const rect = trackRef.current.getBoundingClientRect();
324
- const ratio = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
412
+ const ratio = (clientX - rect.left) / rect.width;
325
413
  const nextReferenceDate = getDateByRatio(trackBounds, ratio);
326
414
  updateRangeByReference(nextReferenceDate, resolvedDefaultRangeDays, { preserveTrack: true });
327
415
  }
328
416
 
329
417
  function applyQuickRange(dayCount: QuickRangeDays) {
330
- updateRangeByReference(referenceDate, dayCount);
418
+ updateRangeByReference(referenceDate, dayCount, { preserveTrack: true });
331
419
  }
332
420
 
333
421
  function shiftReferenceDateByMonths(monthOffset: number) {
@@ -344,6 +432,7 @@ export function RandomDateRangeDialog({
344
432
  pointerIdRef.current = pointerId;
345
433
  dragStartRangeRef.current = { ...draftRange };
346
434
  dragPreviewRef.current = { ...draftRange };
435
+ dragStartTrackBoundsRef.current = { ...trackBoundsRef.current };
347
436
 
348
437
  if (
349
438
  mode === 'window' &&
@@ -351,12 +440,12 @@ export function RandomDateRangeDialog({
351
440
  trackRef.current &&
352
441
  draftRange.startDate &&
353
442
  draftRange.endDate &&
354
- trackBounds.startDate &&
355
- trackBounds.endDate
443
+ dragStartTrackBoundsRef.current.startDate &&
444
+ dragStartTrackBoundsRef.current.endDate
356
445
  ) {
357
446
  const rect = trackRef.current.getBoundingClientRect();
358
- const ratio = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
359
- const pointerDate = getDateByRatio(trackBounds, ratio);
447
+ const ratio = (clientX - rect.left) / rect.width;
448
+ const pointerDate = getDateByRatio(dragStartTrackBoundsRef.current, ratio);
360
449
  windowDragOffsetDaysRef.current = getDaysBetween(draftRange.startDate, pointerDate);
361
450
  } else {
362
451
  windowDragOffsetDaysRef.current = 0;
@@ -364,13 +453,23 @@ export function RandomDateRangeDialog({
364
453
  }
365
454
 
366
455
  const buildDraggedRange = useCallback((clientX: number): RandomCalendarRange | null => {
367
- 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
+ ) {
368
467
  return null;
369
468
  }
370
469
 
371
470
  const rect = trackRef.current.getBoundingClientRect();
372
- const ratio = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
373
- const pointerDate = getDateByRatio(trackBounds, ratio);
471
+ const ratio = (clientX - rect.left) / rect.width;
472
+ const pointerDate = getDateByOverflowRatio(dragStartTrackBounds, ratio, rect.width);
374
473
  const currentRange = dragStartRangeRef.current;
375
474
 
376
475
  if (!currentRange.startDate || !currentRange.endDate) {
@@ -381,24 +480,42 @@ export function RandomDateRangeDialog({
381
480
  const earliestStart = addDays(currentRange.endDate, -(MAX_RANGE_DAYS - 1));
382
481
  const boundedPointerDate = compareDateStrings(pointerDate, earliestStart) < 0 ? earliestStart : pointerDate;
383
482
  const nextStart = compareDateStrings(boundedPointerDate, currentRange.endDate) > 0 ? currentRange.endDate : boundedPointerDate;
384
- 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;
385
491
  }
386
492
 
387
493
  if (dragModeRef.current === 'end') {
388
494
  const latestEnd = addDays(currentRange.startDate, MAX_RANGE_DAYS - 1);
389
495
  const boundedPointerDate = compareDateStrings(pointerDate, latestEnd) > 0 ? latestEnd : pointerDate;
390
496
  const nextEnd = compareDateStrings(boundedPointerDate, currentRange.startDate) < 0 ? currentRange.startDate : boundedPointerDate;
391
- 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;
392
505
  }
393
506
 
394
507
  const spanDays = getDaysBetween(currentRange.startDate, currentRange.endDate);
395
- const nextStart = clampDateToRange(addDays(pointerDate, -windowDragOffsetDaysRef.current), {
396
- startDate: trackBounds.startDate,
397
- endDate: addDays(trackBounds.endDate, -spanDays),
398
- });
508
+ const nextStart = addDays(pointerDate, -windowDragOffsetDaysRef.current);
399
509
  const nextEnd = addDays(nextStart, spanDays);
400
- return { startDate: nextStart, endDate: nextEnd };
401
- }, [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
+ }, []);
402
519
 
403
520
  useEffect(() => {
404
521
  syncPreviewDomRef.current = syncPreviewDom;
@@ -415,6 +532,7 @@ export function RandomDateRangeDialog({
415
532
 
416
533
  const nextRange = dragPreviewRef.current;
417
534
  dragStartRangeRef.current = null;
535
+ dragStartTrackBoundsRef.current = null;
418
536
  dragModeRef.current = null;
419
537
  pointerIdRef.current = null;
420
538
  windowDragOffsetDaysRef.current = 0;
@@ -422,6 +540,7 @@ export function RandomDateRangeDialog({
422
540
  setDraftRange(nextRange);
423
541
  setReferenceDate(nextRange.startDate);
424
542
  setWindowDays(getInclusiveDayCount(nextRange));
543
+ commitTrackBounds(ensureRangeVisibleOnTrack(nextRange, trackBoundsRef.current));
425
544
  }
426
545
  }
427
546
 
@@ -490,9 +609,9 @@ export function RandomDateRangeDialog({
490
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">
491
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">
492
611
  <div className="space-y-5 p-4">
493
- <div className="relative flex items-center justify-center px-16 text-center">
494
- <div ref={resultLabelRef} className="select-none text-base font-semibold text-slate-900 dark:text-white">{getRangeLabel(draftRange)}</div>
495
- <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">
496
615
  <button
497
616
  type="button"
498
617
  onClick={() => onOpenChange(false)}
@@ -501,25 +620,10 @@ export function RandomDateRangeDialog({
501
620
  >
502
621
  <XIcon className="h-4 w-4" />
503
622
  </button>
504
- <button
505
- type="button"
506
- onClick={() => {
507
- void runDialogAction('confirm', handleApply);
508
- }}
509
- disabled={!draftRange.startDate || !draftRange.endDate}
510
- className={cn(
511
- DIALOG_ICON_BUTTON_CLASS_NAME,
512
- 'text-slate-700 dark:text-slate-100',
513
- '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'
514
- )}
515
- aria-label="Apply"
516
- >
517
- <CheckCheckIcon className="h-4 w-4" />
518
- </button>
519
623
  </div>
520
624
  </div>
521
625
 
522
- <div className="space-y-4">
626
+ <div className="space-y-3">
523
627
  <div className="flex items-center justify-between gap-2">
524
628
  <div className="flex items-center gap-1">
525
629
  <button
@@ -569,14 +673,14 @@ export function RandomDateRangeDialog({
569
673
  endDate: addDays(baseReferenceDate, resolvedDefaultRangeDays - 1),
570
674
  };
571
675
  setReferenceDate(baseReferenceDate);
572
- setTrackBounds(buildTrackRange(baseReferenceDate, resolvedDefaultRangeDays));
676
+ commitTrackBounds(buildTrackRange(baseReferenceDate, resolvedDefaultRangeDays));
573
677
  setWindowDays(resolvedDefaultRangeDays);
574
678
  setDraftRange(nextRange);
575
679
  onClear?.(nextRange);
576
680
  }}
577
681
  className={DIALOG_PILL_BUTTON_CLASS_NAME}
578
682
  >
579
- Current Day
683
+ Today
580
684
  </button>
581
685
  <button
582
686
  type="button"
@@ -594,7 +698,8 @@ export function RandomDateRangeDialog({
594
698
  };
595
699
  setDraftRange(normalizedRange);
596
700
  setWindowDays(getInclusiveDayCount(normalizedRange));
597
- setTrackBounds(buildTrackRange(normalizedRange.startDate, getInclusiveDayCount(normalizedRange)));
701
+ setReferenceDate(normalizedRange.startDate);
702
+ commitTrackBounds(buildTrackRange(normalizedRange.startDate, getInclusiveDayCount(normalizedRange)));
598
703
  }}
599
704
  className={DIALOG_PILL_BUTTON_CLASS_NAME}
600
705
  >
@@ -642,10 +747,10 @@ export function RandomDateRangeDialog({
642
747
  </div>
643
748
  </div>
644
749
 
645
- <div className="relative h-24">
750
+ <div className="relative h-21">
646
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">
647
752
  <span className="relative block select-none text-center">
648
- {monthLabels[0] ?? formatMonthShort(trackBounds.startDate ?? baseReferenceDate)}
753
+ <RollingMonthLabel value={leftMonthLabel} />
649
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" />
650
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" />
651
756
  </span>
@@ -667,7 +772,7 @@ export function RandomDateRangeDialog({
667
772
  ))}
668
773
  </div>
669
774
  <span className="relative block select-none text-center">
670
- {monthLabels[1] ?? formatMonthShort(trackBounds.endDate ?? baseReferenceDate)}
775
+ <RollingMonthLabel value={rightMonthLabel} />
671
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" />
672
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" />
673
778
  </span>
@@ -704,8 +809,8 @@ export function RandomDateRangeDialog({
704
809
  </div>
705
810
  <div
706
811
  ref={selectionRef}
707
- 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"
708
- 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 }}
709
814
  onPointerDown={(event) => {
710
815
  event.stopPropagation();
711
816
  beginDrag('window', event.pointerId, event.clientX);
@@ -719,8 +824,8 @@ export function RandomDateRangeDialog({
719
824
  <button
720
825
  ref={startHandleRef}
721
826
  type="button"
722
- 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"
723
- 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 }}
724
829
  onPointerDown={(event) => {
725
830
  event.stopPropagation();
726
831
  beginDrag('start', event.pointerId);
@@ -730,8 +835,8 @@ export function RandomDateRangeDialog({
730
835
  <button
731
836
  ref={endHandleRef}
732
837
  type="button"
733
- 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"
734
- 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 }}
735
840
  onPointerDown={(event) => {
736
841
  event.stopPropagation();
737
842
  beginDrag('end', event.pointerId);
@@ -742,10 +847,61 @@ export function RandomDateRangeDialog({
742
847
  </div>
743
848
  </div>
744
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
+
745
868
  </div>
746
869
  </div>
747
870
  </div>
748
871
  </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>
749
905
  {dialogLoading}
750
906
  </>,
751
907
  document.body
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
  )
@@ -25,12 +25,13 @@ export function DelayedImg({
25
25
  wrapperClassName,
26
26
  placeholderClassName,
27
27
  className,
28
+ onError,
28
29
  onLoad,
29
30
  ...imageProps
30
31
  }: DelayedImgProps) {
31
32
  const shouldDelay = ENV_DELAY_ENABLED && ENV_DELAY_MS > 0
32
33
  const [isMounted, setIsMounted] = useState(!shouldDelay)
33
- const [isLoaded, setIsLoaded] = useState(false)
34
+ const [isSettled, setIsSettled] = useState(false)
34
35
 
35
36
  useEffect(() => {
36
37
  if (!shouldDelay || isMounted) {
@@ -46,7 +47,7 @@ export function DelayedImg({
46
47
 
47
48
  return (
48
49
  <div className={cn("relative", wrapperClassName)}>
49
- {(!isMounted || !isLoaded) && (
50
+ {(!isMounted || !isSettled) && (
50
51
  <SnakeLoadingFrame
51
52
  shape="rounded-rect"
52
53
  loading
@@ -68,13 +69,17 @@ export function DelayedImg({
68
69
  <Image
69
70
  {...imageProps}
70
71
  alt={alt}
72
+ onError={(event) => {
73
+ setIsSettled(true)
74
+ onError?.(event)
75
+ }}
71
76
  onLoad={(event) => {
72
- setIsLoaded(true)
77
+ setIsSettled(true)
73
78
  onLoad?.(event)
74
79
  }}
75
80
  className={cn(
76
81
  "transition duration-300",
77
- isLoaded ? "opacity-100" : "opacity-0",
82
+ isSettled ? "opacity-100" : "opacity-0",
78
83
  className,
79
84
  )}
80
85
  />