@windrun-huaiin/third-ui 29.2.1 → 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 (96) 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.js +1 -1
  11. package/dist/main/alert-dialog/confirm-dialog.mjs +2 -2
  12. package/dist/main/alert-dialog/dialog-loading-action.js +5 -2
  13. package/dist/main/alert-dialog/dialog-loading-action.mjs +5 -2
  14. package/dist/main/alert-dialog/dialog-styles.d.ts +4 -2
  15. package/dist/main/alert-dialog/dialog-styles.js +8 -4
  16. package/dist/main/alert-dialog/dialog-styles.mjs +7 -5
  17. package/dist/main/alert-dialog/high-priority-confirm-dialog.js +5 -5
  18. package/dist/main/alert-dialog/high-priority-confirm-dialog.mjs +6 -6
  19. package/dist/main/alert-dialog/info-dialog.js +1 -1
  20. package/dist/main/alert-dialog/info-dialog.mjs +2 -2
  21. package/dist/main/alert-dialog/undoable-confirm-dialog.js +2 -2
  22. package/dist/main/alert-dialog/undoable-confirm-dialog.mjs +3 -3
  23. package/dist/main/anime/anime-beam-frame.d.ts +3 -0
  24. package/dist/main/anime/anime-beam-frame.js +63 -0
  25. package/dist/main/anime/anime-beam-frame.mjs +61 -0
  26. package/dist/main/anime/anime-spiral-loading.d.ts +10 -0
  27. package/dist/main/anime/anime-spiral-loading.js +77 -0
  28. package/dist/main/anime/anime-spiral-loading.mjs +75 -0
  29. package/dist/main/anime/index.d.ts +2 -0
  30. package/dist/main/anime/index.js +10 -0
  31. package/dist/main/anime/index.mjs +3 -0
  32. package/dist/main/beam-frame/animate.d.ts +3 -0
  33. package/dist/main/beam-frame/animate.js +63 -0
  34. package/dist/main/beam-frame/animate.mjs +61 -0
  35. package/dist/main/beam-frame/beam-frame.d.ts +4 -0
  36. package/dist/main/beam-frame/beam-frame.js +262 -0
  37. package/dist/main/beam-frame/beam-frame.mjs +258 -0
  38. package/dist/main/beam-frame/index.d.ts +4 -0
  39. package/dist/main/beam-frame/index.js +11 -0
  40. package/dist/main/beam-frame/index.mjs +3 -0
  41. package/dist/main/beam-frame/motion.d.ts +3 -0
  42. package/dist/main/beam-frame/motion.js +61 -0
  43. package/dist/main/beam-frame/motion.mjs +59 -0
  44. package/dist/main/beam-frame/share-config.d.ts +54 -0
  45. package/dist/main/beam-frame/share-config.js +161 -0
  46. package/dist/main/beam-frame/share-config.mjs +152 -0
  47. package/dist/main/beam-frame-config.d.ts +54 -0
  48. package/dist/main/beam-frame-config.js +161 -0
  49. package/dist/main/beam-frame-config.mjs +152 -0
  50. package/dist/main/calendar/random-date-range-dialog.js +177 -51
  51. package/dist/main/calendar/random-date-range-dialog.mjs +178 -52
  52. package/dist/main/cta.js +17 -1
  53. package/dist/main/cta.mjs +18 -2
  54. package/dist/main/delayed-img.d.ts +1 -1
  55. package/dist/main/delayed-img.js +8 -5
  56. package/dist/main/delayed-img.mjs +8 -5
  57. package/dist/main/info-tooltip.js +70 -9
  58. package/dist/main/info-tooltip.mjs +70 -9
  59. package/dist/main/loading-frame/index.d.ts +1 -0
  60. package/dist/main/loading.d.ts +2 -1
  61. package/dist/main/loading.js +64 -26
  62. package/dist/main/loading.mjs +64 -26
  63. package/dist/main/motion/index.d.ts +1 -0
  64. package/dist/main/motion/index.js +9 -0
  65. package/dist/main/motion/index.mjs +2 -0
  66. package/dist/main/motion/motion-beam-frame.d.ts +3 -0
  67. package/dist/main/motion/motion-beam-frame.js +61 -0
  68. package/dist/main/motion/motion-beam-frame.mjs +59 -0
  69. package/dist/main/snake-loading-frame.d.ts +7 -3
  70. package/dist/main/snake-loading-frame.js +44 -252
  71. package/dist/main/snake-loading-frame.mjs +46 -254
  72. package/package.json +16 -5
  73. package/src/fuma/mdx/cheet-table.tsx +650 -0
  74. package/src/fuma/mdx/index.ts +1 -0
  75. package/src/fuma/server/features/widgets.tsx +2 -0
  76. package/src/main/alert-dialog/confirm-dialog.tsx +2 -1
  77. package/src/main/alert-dialog/dialog-loading-action.tsx +7 -5
  78. package/src/main/alert-dialog/dialog-styles.ts +13 -3
  79. package/src/main/alert-dialog/high-priority-confirm-dialog.tsx +26 -23
  80. package/src/main/alert-dialog/info-dialog.tsx +2 -1
  81. package/src/main/alert-dialog/undoable-confirm-dialog.tsx +18 -17
  82. package/src/main/anime/anime-beam-frame.tsx +128 -0
  83. package/src/main/anime/anime-spiral-loading.tsx +123 -0
  84. package/src/main/anime/index.ts +9 -0
  85. package/src/main/beam-frame-config.tsx +341 -0
  86. package/src/main/calendar/random-date-range-dialog.tsx +225 -69
  87. package/src/main/cta.tsx +50 -21
  88. package/src/main/delayed-img.tsx +9 -4
  89. package/src/main/info-tooltip.tsx +116 -20
  90. package/src/main/loading-frame/index.ts +4 -0
  91. package/src/main/loading.tsx +75 -24
  92. package/src/main/motion/index.ts +8 -0
  93. package/src/main/motion/motion-beam-frame.tsx +137 -0
  94. package/src/main/snake-loading-frame.tsx +95 -496
  95. package/src/styles/cta.css +21 -4
  96. package/src/styles/third-ui.css +0 -20
@@ -1,43 +1,42 @@
1
1
  'use client';
2
2
 
3
- import type { ReactNode, RefObject } from 'react';
4
- import { useEffect, useId, useMemo, useRef, useState } from 'react';
5
- import { motion } from 'framer-motion';
3
+ import type { ReactNode } from 'react';
4
+ import { useEffect, useRef, useState } from 'react';
5
+ import { motion } from 'motion/react';
6
6
  import { cn } from '@windrun-huaiin/lib/utils';
7
+ import { AnimeBeamFrame } from './anime';
8
+ import type { BeamFrameTone } from './beam-frame-config';
7
9
 
8
10
  type SnakeShape = 'circle' | 'rounded-rect';
9
11
 
10
- interface SnakeLoadingFrameProps {
12
+ export interface SnakeLoadingFrameProps {
11
13
  shape: SnakeShape;
12
14
  loading: boolean;
13
15
  children: ReactNode;
16
+ paused?: boolean;
14
17
  className?: string;
15
18
  themeColor?: string;
19
+ tone?: BeamFrameTone;
16
20
  strokeWidth?: number;
17
21
  contentClassName?: string;
18
22
  }
19
23
 
20
- interface SnakeLoadingPreviewProps {
24
+ export interface SnakeLoadingPreviewProps {
21
25
  shape: SnakeShape;
22
26
  children: ReactNode;
23
27
  className?: string;
24
28
  themeColor?: string;
29
+ tone?: BeamFrameTone;
25
30
  defaultProgress?: number;
26
31
  strokeWidth?: number;
27
32
  contentClassName?: string;
28
33
  }
29
34
 
30
35
  const DEFAULT_THEME_COLOR = '#3b82f6';
31
- const TRACK_COLOR = 'rgba(148, 163, 184, 0.22)';
32
- const BODY_LENGTH_RATIO = 0.26;
33
36
  const EXIT_DURATION_MS = 260;
34
37
  const LOOP_DURATION_SECONDS = 1.85;
35
38
  const DEFAULT_CIRCLE_STROKE = 0.5;
36
39
  const DEFAULT_RECT_STROKE = 1;
37
- const MIN_FRAME_SIZE = 2;
38
- const MIN_BODY_LENGTH = 24;
39
- const MAX_BODY_LENGTH_RATIO = 0.36;
40
- const RECT_MIN_STROKE_WIDTH = 1;
41
40
 
42
41
  function clampProgress(progress: number): number {
43
42
  if (!Number.isFinite(progress)) {
@@ -71,488 +70,34 @@ function createBodyTailColor(themeColor: string): string {
71
70
  return `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, 0.18)`;
72
71
  }
73
72
 
74
- function createHeadGlowColor(themeColor: string): string {
75
- const rgb = hexToRgb(themeColor);
76
-
77
- if (!rgb) {
78
- return 'rgba(59, 130, 246, 0.94)';
79
- }
80
-
81
- return `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, 0.94)`;
82
- }
83
-
84
- function createSweepTailColor(themeColor: string): string {
85
- const rgb = hexToRgb(themeColor);
86
-
87
- if (!rgb) {
88
- return 'rgba(59, 130, 246, 0.32)';
89
- }
90
-
91
- return `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, 0.32)`;
92
- }
93
-
94
- interface CornerRadius {
95
- x: number;
96
- y: number;
97
- }
98
-
99
- interface RectRadii {
100
- topLeft: CornerRadius;
101
- topRight: CornerRadius;
102
- bottomRight: CornerRadius;
103
- bottomLeft: CornerRadius;
104
- }
105
-
106
- interface RingGeometry {
107
- viewBox: string;
108
- path: string;
109
- length: number;
110
- strokeWidth: number;
111
- }
112
-
113
- function parseRadiusValue(value: string): CornerRadius {
114
- const parts = value
115
- .split(/\s+/)
116
- .map((part) => Number.parseFloat(part))
117
- .filter((part) => Number.isFinite(part));
118
-
119
- if (parts.length === 0) {
120
- return { x: 0, y: 0 };
121
- }
122
-
123
- if (parts.length === 1) {
124
- return { x: parts[0], y: parts[0] };
125
- }
126
-
127
- return { x: parts[0], y: parts[1] };
128
- }
129
-
130
- function clampCornerRadius(
131
- radius: CornerRadius,
132
- maxX: number,
133
- maxY: number,
134
- ): CornerRadius {
135
- return {
136
- x: Math.max(0, Math.min(radius.x, maxX)),
137
- y: Math.max(0, Math.min(radius.y, maxY)),
138
- };
139
- }
140
-
141
- function scaleHorizontalPair(
142
- start: CornerRadius,
143
- end: CornerRadius,
144
- limit: number,
145
- ): [CornerRadius, CornerRadius] {
146
- const sum = start.x + end.x;
147
-
148
- if (sum <= limit || sum === 0) {
149
- return [start, end];
150
- }
151
-
152
- const scale = limit / sum;
153
-
154
- return [
155
- { ...start, x: start.x * scale },
156
- { ...end, x: end.x * scale },
157
- ];
158
- }
159
-
160
- function scaleVerticalPair(
161
- start: CornerRadius,
162
- end: CornerRadius,
163
- limit: number,
164
- ): [CornerRadius, CornerRadius] {
165
- const sum = start.y + end.y;
166
-
167
- if (sum <= limit || sum === 0) {
168
- return [start, end];
169
- }
170
-
171
- const scale = limit / sum;
172
-
173
- return [
174
- { ...start, y: start.y * scale },
175
- { ...end, y: end.y * scale },
176
- ];
177
- }
178
-
179
- function normalizeRectRadii(
180
- width: number,
181
- height: number,
182
- input: RectRadii,
183
- ): RectRadii {
184
- let topLeft = clampCornerRadius(input.topLeft, width / 2, height / 2);
185
- let topRight = clampCornerRadius(input.topRight, width / 2, height / 2);
186
- let bottomRight = clampCornerRadius(input.bottomRight, width / 2, height / 2);
187
- let bottomLeft = clampCornerRadius(input.bottomLeft, width / 2, height / 2);
188
-
189
- [topLeft, topRight] = scaleHorizontalPair(topLeft, topRight, width);
190
- [bottomLeft, bottomRight] = scaleHorizontalPair(bottomLeft, bottomRight, width);
191
- [topLeft, bottomLeft] = scaleVerticalPair(topLeft, bottomLeft, height);
192
- [topRight, bottomRight] = scaleVerticalPair(topRight, bottomRight, height);
193
-
194
- return {
195
- topLeft,
196
- topRight,
197
- bottomRight,
198
- bottomLeft,
199
- };
200
- }
201
-
202
- function buildRoundedRectPath(
203
- x: number,
204
- y: number,
205
- width: number,
206
- height: number,
207
- radii: RectRadii,
208
- ): string {
209
- const { topLeft, topRight, bottomRight, bottomLeft } = normalizeRectRadii(
210
- width,
211
- height,
212
- radii,
213
- );
214
-
215
- return [
216
- `M ${x + topLeft.x} ${y}`,
217
- `H ${x + Math.max(topLeft.x, width - topRight.x)}`,
218
- topRight.x > 0 || topRight.y > 0
219
- ? `A ${topRight.x} ${topRight.y} 0 0 1 ${x + width} ${y + topRight.y}`
220
- : `L ${x + width} ${y}`,
221
- `V ${y + Math.max(topRight.y, height - bottomRight.y)}`,
222
- bottomRight.x > 0 || bottomRight.y > 0
223
- ? `A ${bottomRight.x} ${bottomRight.y} 0 0 1 ${x + width - bottomRight.x} ${y + height}`
224
- : `L ${x + width} ${y + height}`,
225
- `H ${x + Math.min(width - bottomRight.x, bottomLeft.x)}`,
226
- bottomLeft.x > 0 || bottomLeft.y > 0
227
- ? `A ${bottomLeft.x} ${bottomLeft.y} 0 0 1 ${x} ${y + height - bottomLeft.y}`
228
- : `L ${x} ${y + height}`,
229
- `V ${y + Math.min(height - bottomLeft.y, topLeft.y)}`,
230
- topLeft.x > 0 || topLeft.y > 0
231
- ? `A ${topLeft.x} ${topLeft.y} 0 0 1 ${x + topLeft.x} ${y}`
232
- : `L ${x} ${y}`,
233
- 'Z',
234
- ].join(' ');
235
- }
236
-
237
- function createCircleGeometry(
238
- width: number,
239
- height: number,
240
- strokeWidth: number,
241
- ): RingGeometry {
242
- const safeWidth = Math.max(width, MIN_FRAME_SIZE);
243
- const safeHeight = Math.max(height, MIN_FRAME_SIZE);
244
- const radius = Math.max(0, Math.min(safeWidth, safeHeight) / 2 - strokeWidth / 2);
245
- const centerX = safeWidth / 2;
246
- const centerY = safeHeight / 2;
247
-
248
- return {
249
- viewBox: `0 0 ${safeWidth} ${safeHeight}`,
250
- path: `M ${centerX} ${centerY - radius} A ${radius} ${radius} 0 1 1 ${centerX} ${
251
- centerY + radius
252
- } A ${radius} ${radius} 0 1 1 ${centerX} ${centerY - radius}`,
253
- length: Math.max(0, 2 * Math.PI * radius),
254
- strokeWidth,
255
- };
256
- }
257
-
258
- function createRoundedRectGeometry(
259
- width: number,
260
- height: number,
261
- strokeWidth: number,
262
- radii: RectRadii,
263
- ): RingGeometry {
264
- const safeWidth = Math.max(width, MIN_FRAME_SIZE);
265
- const safeHeight = Math.max(height, MIN_FRAME_SIZE);
266
- const inset = strokeWidth / 2;
267
- const innerWidth = Math.max(safeWidth - strokeWidth, MIN_FRAME_SIZE);
268
- const innerHeight = Math.max(safeHeight - strokeWidth, MIN_FRAME_SIZE);
269
- const adjustedRadii = normalizeRectRadii(innerWidth, innerHeight, {
270
- topLeft: {
271
- x: Math.max(0, radii.topLeft.x - inset),
272
- y: Math.max(0, radii.topLeft.y - inset),
273
- },
274
- topRight: {
275
- x: Math.max(0, radii.topRight.x - inset),
276
- y: Math.max(0, radii.topRight.y - inset),
277
- },
278
- bottomRight: {
279
- x: Math.max(0, radii.bottomRight.x - inset),
280
- y: Math.max(0, radii.bottomRight.y - inset),
281
- },
282
- bottomLeft: {
283
- x: Math.max(0, radii.bottomLeft.x - inset),
284
- y: Math.max(0, radii.bottomLeft.y - inset),
285
- },
286
- });
287
-
288
- return {
289
- viewBox: `0 0 ${safeWidth} ${safeHeight}`,
290
- path: buildRoundedRectPath(inset, inset, innerWidth, innerHeight, adjustedRadii),
291
- length: 0,
292
- strokeWidth,
293
- };
294
- }
295
-
296
- function readRectRadii(element: HTMLElement): RectRadii {
297
- const computedStyle = window.getComputedStyle(element);
298
-
299
- return {
300
- topLeft: parseRadiusValue(computedStyle.borderTopLeftRadius),
301
- topRight: parseRadiusValue(computedStyle.borderTopRightRadius),
302
- bottomRight: parseRadiusValue(computedStyle.borderBottomRightRadius),
303
- bottomLeft: parseRadiusValue(computedStyle.borderBottomLeftRadius),
304
- };
305
- }
306
-
307
- function SnakeRingSvg({
308
- containerRef,
73
+ function StaticProgressFrame({
309
74
  shape,
75
+ progress,
310
76
  themeColor,
311
- progressRatio,
312
- animate,
313
- strokeWidth,
314
77
  }: {
315
- containerRef: RefObject<HTMLDivElement | null>;
316
78
  shape: SnakeShape;
79
+ progress: number;
317
80
  themeColor: string;
318
- progressRatio: number;
319
- animate: boolean;
320
- strokeWidth?: number;
321
81
  }) {
322
- const gradientId = useId().replace(/:/g, '-');
323
- const tailColor = createBodyTailColor(themeColor);
324
- const headGlowColor = createHeadGlowColor(themeColor);
325
- const sweepTailColor = createSweepTailColor(themeColor);
326
- const [geometry, setGeometry] = useState<RingGeometry | null>(null);
327
- const pathMeasureRef = useRef<SVGPathElement | null>(null);
328
- const measuredLengthRef = useRef(0);
329
-
330
- useEffect(() => {
331
- const container = containerRef.current;
332
-
333
- if (!container) {
334
- return;
335
- }
336
-
337
- const updateGeometry = () => {
338
- const rect = container.getBoundingClientRect();
339
- const preferredStrokeWidth =
340
- strokeWidth ?? (shape === 'circle' ? DEFAULT_CIRCLE_STROKE : DEFAULT_RECT_STROKE);
341
- const resolvedStrokeWidth =
342
- shape === 'rounded-rect'
343
- ? Math.max(RECT_MIN_STROKE_WIDTH, preferredStrokeWidth)
344
- : preferredStrokeWidth;
345
- const nextGeometry =
346
- shape === 'circle'
347
- ? createCircleGeometry(rect.width, rect.height, resolvedStrokeWidth)
348
- : createRoundedRectGeometry(
349
- rect.width,
350
- rect.height,
351
- resolvedStrokeWidth,
352
- readRectRadii(container),
353
- );
354
-
355
- measuredLengthRef.current = 0;
356
- setGeometry(nextGeometry);
357
- };
358
-
359
- updateGeometry();
360
-
361
- const resizeObserver = new ResizeObserver(() => {
362
- updateGeometry();
363
- });
364
- const mutationObserver = new MutationObserver(() => {
365
- updateGeometry();
366
- });
367
-
368
- resizeObserver.observe(container);
369
- mutationObserver.observe(container, {
370
- attributes: true,
371
- attributeFilter: ['class', 'style'],
372
- });
373
-
374
- return () => {
375
- resizeObserver.disconnect();
376
- mutationObserver.disconnect();
377
- };
378
- }, [containerRef, shape, strokeWidth]);
379
-
380
- useEffect(() => {
381
- if (!geometry || shape === 'circle' || !pathMeasureRef.current) {
382
- return;
383
- }
384
-
385
- const measuredLength = pathMeasureRef.current.getTotalLength();
386
- const normalizedLength = Number.isFinite(measuredLength)
387
- ? Math.max(0, measuredLength)
388
- : 0;
389
-
390
- if (normalizedLength <= 0) {
391
- return;
392
- }
393
-
394
- if (Math.abs(measuredLengthRef.current - normalizedLength) < 0.1) {
395
- return;
396
- }
397
-
398
- measuredLengthRef.current = normalizedLength;
399
-
400
- setGeometry((current) => {
401
- if (!current || current.path !== geometry.path) {
402
- return current;
403
- }
404
-
405
- if (Math.abs(current.length - normalizedLength) < 0.1) {
406
- return current;
407
- }
408
-
409
- return {
410
- ...current,
411
- length: normalizedLength,
412
- };
413
- });
414
- }, [geometry, shape]);
415
-
416
- const resolvedGeometry = useMemo(() => geometry, [geometry]);
417
-
418
- if (!resolvedGeometry) {
419
- return null;
420
- }
421
-
422
- const effectiveLength =
423
- resolvedGeometry.length > 0
424
- ? resolvedGeometry.length
425
- : Math.max(
426
- 1,
427
- 2 * Math.max(0, resolvedGeometry.strokeWidth),
428
- 2 *
429
- (Math.max(0, Number.parseFloat(resolvedGeometry.viewBox.split(' ')[2] ?? '0')) +
430
- Math.max(0, Number.parseFloat(resolvedGeometry.viewBox.split(' ')[3] ?? '0'))),
431
- );
432
- const [, , viewBoxWidthRaw, viewBoxHeightRaw] = resolvedGeometry.viewBox.split(' ');
433
- const viewBoxWidth = Math.max(0, Number.parseFloat(viewBoxWidthRaw ?? '0'));
434
- const viewBoxHeight = Math.max(0, Number.parseFloat(viewBoxHeightRaw ?? '0'));
435
- const centerX = viewBoxWidth / 2;
436
- const centerY = viewBoxHeight / 2;
437
- const isCircle = shape === 'circle';
438
- const tailTransparentStart = isCircle ? '18%' : '26%';
439
- const tailColorStart = isCircle ? '39%' : '46%';
440
- const tailTransparentEnd = isCircle ? '90%' : '82%';
441
- const headTransparentStart = isCircle ? '32%' : '40%';
442
- const headColorStart = isCircle ? '48%' : '53%';
443
- const headTransparentEnd = isCircle ? '82%' : '73%';
444
- const bodyLength = Math.min(
445
- effectiveLength * MAX_BODY_LENGTH_RATIO,
446
- Math.max(MIN_BODY_LENGTH, effectiveLength * BODY_LENGTH_RATIO),
447
- );
448
- const bodyDashArray = `${bodyLength} ${effectiveLength}`;
449
- const staticDashOffset = effectiveLength * (1 - progressRatio);
450
- const tailStrokeWidth =
451
- shape === 'circle'
452
- ? resolvedGeometry.strokeWidth + 1.2
453
- : resolvedGeometry.strokeWidth + Math.min(1.2, resolvedGeometry.strokeWidth * 0.32);
82
+ const trackColor = 'rgba(148, 163, 184, 0.22)';
83
+ const progressRatio = clampProgress(progress);
454
84
 
455
85
  return (
456
- <svg
457
- className="pointer-events-none absolute inset-0 h-full w-full"
458
- viewBox={resolvedGeometry.viewBox}
459
- preserveAspectRatio="none"
86
+ <div
460
87
  aria-hidden="true"
461
- >
462
- {shape === 'rounded-rect' ? (
463
- <path ref={pathMeasureRef} d={resolvedGeometry.path} fill="none" stroke="transparent" />
464
- ) : null}
465
- {animate ? (
466
- <defs>
467
- <linearGradient
468
- id={`${gradientId}-sweep-tail`}
469
- x1="0"
470
- y1="0"
471
- x2={String(viewBoxWidth)}
472
- y2="0"
473
- gradientUnits="userSpaceOnUse"
474
- >
475
- <stop offset="0%" stopColor="transparent" />
476
- <stop offset={tailTransparentStart} stopColor="transparent" />
477
- <stop offset={tailColorStart} stopColor={sweepTailColor} />
478
- <stop offset="64%" stopColor={headGlowColor} />
479
- <stop offset={tailTransparentEnd} stopColor="transparent" />
480
- <stop offset="100%" stopColor="transparent" />
481
- <animateTransform
482
- attributeName="gradientTransform"
483
- type="rotate"
484
- from={`0 ${centerX} ${centerY}`}
485
- to={`360 ${centerX} ${centerY}`}
486
- dur={`${LOOP_DURATION_SECONDS}s`}
487
- repeatCount="indefinite"
488
- />
489
- </linearGradient>
490
- <linearGradient
491
- id={`${gradientId}-sweep-head`}
492
- x1="0"
493
- y1="0"
494
- x2={String(viewBoxWidth)}
495
- y2="0"
496
- gradientUnits="userSpaceOnUse"
497
- >
498
- <stop offset="0%" stopColor="transparent" />
499
- <stop offset={headTransparentStart} stopColor="transparent" />
500
- <stop offset={headColorStart} stopColor={themeColor} />
501
- <stop offset="63%" stopColor={headGlowColor} />
502
- <stop offset={headTransparentEnd} stopColor="transparent" />
503
- <stop offset="100%" stopColor="transparent" />
504
- <animateTransform
505
- attributeName="gradientTransform"
506
- type="rotate"
507
- from={`0 ${centerX} ${centerY}`}
508
- to={`360 ${centerX} ${centerY}`}
509
- dur={`${LOOP_DURATION_SECONDS}s`}
510
- repeatCount="indefinite"
511
- />
512
- </linearGradient>
513
- </defs>
514
- ) : null}
515
- <path
516
- d={resolvedGeometry.path}
517
- fill="none"
518
- stroke={TRACK_COLOR}
519
- strokeWidth={resolvedGeometry.strokeWidth}
520
- strokeLinecap="round"
521
- strokeLinejoin="round"
522
- />
523
- {animate ? (
524
- <>
525
- <path
526
- d={resolvedGeometry.path}
527
- fill="none"
528
- stroke={`url(#${gradientId}-sweep-tail)`}
529
- strokeWidth={tailStrokeWidth}
530
- strokeLinecap="round"
531
- strokeLinejoin="round"
532
- />
533
- <path
534
- d={resolvedGeometry.path}
535
- fill="none"
536
- stroke={`url(#${gradientId}-sweep-head)`}
537
- strokeWidth={resolvedGeometry.strokeWidth}
538
- strokeLinecap="round"
539
- strokeLinejoin="round"
540
- />
541
- </>
542
- ) : null}
543
- {!animate ? (
544
- <path
545
- d={resolvedGeometry.path}
546
- fill="none"
547
- stroke={themeColor}
548
- strokeWidth={resolvedGeometry.strokeWidth}
549
- strokeLinecap="round"
550
- strokeLinejoin="round"
551
- strokeDasharray={bodyDashArray}
552
- strokeDashoffset={staticDashOffset}
553
- />
554
- ) : null}
555
- </svg>
88
+ className={cn(
89
+ 'pointer-events-none absolute inset-0 p-px',
90
+ shape === 'circle' ? 'rounded-full' : 'rounded-[inherit]',
91
+ )}
92
+ style={{
93
+ background: `conic-gradient(${themeColor} 0% ${progressRatio}%, ${trackColor} ${progressRatio}% 100%)`,
94
+ mask: 'linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0)',
95
+ maskComposite: 'exclude',
96
+ WebkitMask:
97
+ 'linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0)',
98
+ WebkitMaskComposite: 'xor',
99
+ }}
100
+ />
556
101
  );
557
102
  }
558
103
 
@@ -560,23 +105,65 @@ function SnakeFrameBase({
560
105
  shape,
561
106
  loading,
562
107
  children,
108
+ paused = false,
563
109
  className,
564
110
  themeColor = DEFAULT_THEME_COLOR,
111
+ tone = 'rainbow',
565
112
  previewProgress,
566
113
  strokeWidth,
567
114
  contentClassName,
568
115
  }: SnakeLoadingFrameProps & { previewProgress?: number }) {
569
116
  const containerRef = useRef<HTMLDivElement | null>(null);
117
+ const [frameRadius, setFrameRadius] = useState<number | undefined>(
118
+ shape === 'circle' ? 999 : undefined,
119
+ );
570
120
  const [showOverlay, setShowOverlay] = useState(loading || previewProgress !== undefined);
571
121
  const exitTimerRef = useRef<number | null>(null);
572
122
  const resolvedStrokeWidth =
573
123
  strokeWidth ?? (shape === 'circle' ? DEFAULT_CIRCLE_STROKE : DEFAULT_RECT_STROKE);
574
124
  const circleContentInset =
575
125
  shape === 'circle' ? Math.max(6, resolvedStrokeWidth + 4) : 0;
576
- const progressRatio = clampProgress(previewProgress ?? 0) / 100;
126
+ const isPreview = previewProgress !== undefined;
127
+ const isActivelyLoading = loading && !paused && !isPreview;
577
128
 
578
129
  useEffect(() => {
579
- if (previewProgress !== undefined) {
130
+ if (shape === 'circle') {
131
+ setFrameRadius(999);
132
+ return;
133
+ }
134
+
135
+ const node = containerRef.current;
136
+
137
+ if (!node) {
138
+ return;
139
+ }
140
+
141
+ const updateRadius = () => {
142
+ const computedStyle = window.getComputedStyle(node);
143
+ const nextRadius = Number.parseFloat(computedStyle.borderTopLeftRadius);
144
+
145
+ setFrameRadius(Number.isFinite(nextRadius) ? nextRadius : undefined);
146
+ };
147
+
148
+ updateRadius();
149
+
150
+ const resizeObserver = new ResizeObserver(updateRadius);
151
+ const mutationObserver = new MutationObserver(updateRadius);
152
+
153
+ resizeObserver.observe(node);
154
+ mutationObserver.observe(node, {
155
+ attributes: true,
156
+ attributeFilter: ['class', 'style'],
157
+ });
158
+
159
+ return () => {
160
+ resizeObserver.disconnect();
161
+ mutationObserver.disconnect();
162
+ };
163
+ }, [shape]);
164
+
165
+ useEffect(() => {
166
+ if (isPreview) {
580
167
  setShowOverlay(true);
581
168
  return;
582
169
  }
@@ -602,7 +189,7 @@ function SnakeFrameBase({
602
189
  exitTimerRef.current = null;
603
190
  }
604
191
  };
605
- }, [loading, previewProgress]);
192
+ }, [isPreview, loading]);
606
193
 
607
194
  return (
608
195
  <div
@@ -635,17 +222,27 @@ function SnakeFrameBase({
635
222
  <motion.div
636
223
  className="pointer-events-none absolute inset-0 overflow-visible"
637
224
  initial={false}
638
- animate={{ opacity: loading || previewProgress !== undefined ? 1 : 0 }}
225
+ animate={{ opacity: loading || isPreview ? 1 : 0 }}
639
226
  transition={{ duration: EXIT_DURATION_MS / 1000, ease: 'easeOut' }}
640
227
  >
641
- <SnakeRingSvg
642
- containerRef={containerRef}
643
- shape={shape}
644
- themeColor={themeColor}
645
- progressRatio={progressRatio}
646
- animate={previewProgress === undefined && loading}
647
- strokeWidth={resolvedStrokeWidth}
648
- />
228
+ {isPreview ? (
229
+ <StaticProgressFrame
230
+ shape={shape}
231
+ progress={previewProgress}
232
+ themeColor={themeColor}
233
+ />
234
+ ) : (
235
+ <AnimeBeamFrame
236
+ active={isActivelyLoading}
237
+ interactive={false}
238
+ tone={tone}
239
+ duration={LOOP_DURATION_SECONDS}
240
+ radius={frameRadius}
241
+ className="absolute inset-0 h-full w-full"
242
+ >
243
+ <div className="h-full w-full" />
244
+ </AnimeBeamFrame>
245
+ )}
649
246
  </motion.div>
650
247
  ) : null}
651
248
  </div>
@@ -661,6 +258,7 @@ export function SnakeLoadingPreview({
661
258
  children,
662
259
  className,
663
260
  themeColor = DEFAULT_THEME_COLOR,
261
+ tone = 'rainbow',
664
262
  defaultProgress = 32,
665
263
  strokeWidth,
666
264
  contentClassName,
@@ -678,6 +276,7 @@ export function SnakeLoadingPreview({
678
276
  previewProgress={progress}
679
277
  className={className}
680
278
  themeColor={themeColor}
279
+ tone={tone}
681
280
  strokeWidth={strokeWidth}
682
281
  contentClassName={contentClassName}
683
282
  >