@echothink-ui/motion 0.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.
- package/README.md +5 -0
- package/dist/components/AgentThinkingAnimation.d.ts +2 -0
- package/dist/components/AttentionPulse.d.ts +2 -0
- package/dist/components/DAGStatusTransition.d.ts +2 -0
- package/dist/components/DocumentLockPulse.d.ts +2 -0
- package/dist/components/PipelineFlowAnimation.d.ts +2 -0
- package/dist/components/ProgressTransition.d.ts +2 -0
- package/dist/components/SkeletonLoadingPattern.d.ts +2 -0
- package/dist/components/StatusChangeAnimation.d.ts +2 -0
- package/dist/components/StepCompletionAnimation.d.ts +2 -0
- package/dist/components/StreamingText.d.ts +2 -0
- package/dist/components/SyncProgressAnimation.d.ts +2 -0
- package/dist/components/motionUtils.d.ts +5 -0
- package/dist/components/types.d.ts +82 -0
- package/dist/index.cjs +2381 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +2333 -0
- package/dist/index.js.map +1 -0
- package/package.json +38 -0
- package/src/components/AgentThinkingAnimation.tsx +59 -0
- package/src/components/AttentionPulse.tsx +57 -0
- package/src/components/DAGStatusTransition.tsx +292 -0
- package/src/components/DocumentLockPulse.tsx +72 -0
- package/src/components/PipelineFlowAnimation.tsx +243 -0
- package/src/components/ProgressTransition.tsx +51 -0
- package/src/components/SkeletonLoadingPattern.tsx +248 -0
- package/src/components/StatusChangeAnimation.test.tsx +20 -0
- package/src/components/StatusChangeAnimation.tsx +89 -0
- package/src/components/StepCompletionAnimation.tsx +75 -0
- package/src/components/StreamingText.tsx +77 -0
- package/src/components/SyncProgressAnimation.test.tsx +49 -0
- package/src/components/SyncProgressAnimation.tsx +256 -0
- package/src/components/motionUtils.tsx +942 -0
- package/src/components/types.ts +111 -0
- package/src/index.test.tsx +97 -0
- package/src/index.tsx +44 -0
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import type { SyncProgressAnimationProps, SyncProgressAnimationStatus } from "./types";
|
|
3
|
+
import { MotionStyles, usePrefersReducedMotion } from "./motionUtils";
|
|
4
|
+
|
|
5
|
+
const statusContent: Record<
|
|
6
|
+
SyncProgressAnimationStatus,
|
|
7
|
+
{ accent: string; description: string; label: string; soft: string }
|
|
8
|
+
> = {
|
|
9
|
+
syncing: {
|
|
10
|
+
accent: "#0f62fe",
|
|
11
|
+
description: "Replicating source updates and applying remote changes.",
|
|
12
|
+
label: "Syncing",
|
|
13
|
+
soft: "#edf5ff"
|
|
14
|
+
},
|
|
15
|
+
synced: {
|
|
16
|
+
accent: "#24a148",
|
|
17
|
+
description: "Replica is current with the source.",
|
|
18
|
+
label: "Synced",
|
|
19
|
+
soft: "#defbe6"
|
|
20
|
+
},
|
|
21
|
+
paused: {
|
|
22
|
+
accent: "#8d8d8d",
|
|
23
|
+
description: "Sync is paused. No changes are being applied.",
|
|
24
|
+
label: "Paused",
|
|
25
|
+
soft: "#f4f4f4"
|
|
26
|
+
},
|
|
27
|
+
stale: {
|
|
28
|
+
accent: "#f1c21b",
|
|
29
|
+
description: "Replica is outside the expected freshness window.",
|
|
30
|
+
label: "Stale",
|
|
31
|
+
soft: "#fcf4d6"
|
|
32
|
+
},
|
|
33
|
+
failed: {
|
|
34
|
+
accent: "#da1e28",
|
|
35
|
+
description: "Sync stopped before completion. Review the connection and retry.",
|
|
36
|
+
label: "Failed",
|
|
37
|
+
soft: "#fff1f1"
|
|
38
|
+
},
|
|
39
|
+
error: {
|
|
40
|
+
accent: "#da1e28",
|
|
41
|
+
description: "Sync stopped before completion. Review the connection and retry.",
|
|
42
|
+
label: "Needs attention",
|
|
43
|
+
soft: "#fff1f1"
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export function SyncProgressAnimation({
|
|
48
|
+
children,
|
|
49
|
+
className,
|
|
50
|
+
completed,
|
|
51
|
+
description,
|
|
52
|
+
label = "Syncing",
|
|
53
|
+
progress,
|
|
54
|
+
rate,
|
|
55
|
+
remaining,
|
|
56
|
+
role = "status",
|
|
57
|
+
source,
|
|
58
|
+
stage,
|
|
59
|
+
status = "syncing",
|
|
60
|
+
style,
|
|
61
|
+
target,
|
|
62
|
+
total,
|
|
63
|
+
variant,
|
|
64
|
+
"aria-busy": ariaBusy,
|
|
65
|
+
"aria-label": ariaLabel,
|
|
66
|
+
"aria-live": ariaLive,
|
|
67
|
+
...props
|
|
68
|
+
}: SyncProgressAnimationProps) {
|
|
69
|
+
const reduced = usePrefersReducedMotion();
|
|
70
|
+
const content = statusContent[status];
|
|
71
|
+
const progressValue = normalizeProgress(progress ?? progressFromCounts(completed, total, status));
|
|
72
|
+
const hasProgress = progressValue !== undefined;
|
|
73
|
+
const progressText = hasProgress ? `${progressValue}% complete` : content.label;
|
|
74
|
+
const countText = formatCountProgress(completed, total);
|
|
75
|
+
const descriptionNode = description ?? children ?? content.description;
|
|
76
|
+
const hasDetailedContent = [
|
|
77
|
+
description,
|
|
78
|
+
children,
|
|
79
|
+
source,
|
|
80
|
+
target,
|
|
81
|
+
stage,
|
|
82
|
+
rate,
|
|
83
|
+
remaining,
|
|
84
|
+
completed,
|
|
85
|
+
total,
|
|
86
|
+
progress
|
|
87
|
+
].some(hasValue);
|
|
88
|
+
const resolvedVariant = variant ?? (hasDetailedContent ? "detailed" : "compact");
|
|
89
|
+
const labelText = textFromNode(label) ?? "Sync";
|
|
90
|
+
const announcement = [content.label, hasProgress ? progressText : undefined, countText]
|
|
91
|
+
.filter(Boolean)
|
|
92
|
+
.join(", ");
|
|
93
|
+
const classes = [
|
|
94
|
+
"eth-motion-sync-progress",
|
|
95
|
+
`eth-motion-sync-progress--${resolvedVariant}`,
|
|
96
|
+
`eth-motion-sync-progress--${status}`,
|
|
97
|
+
reduced ? "eth-motion-reduced" : undefined,
|
|
98
|
+
className
|
|
99
|
+
]
|
|
100
|
+
.filter(Boolean)
|
|
101
|
+
.join(" ");
|
|
102
|
+
const rootStyle = {
|
|
103
|
+
"--eth-motion-sync-accent": content.accent,
|
|
104
|
+
"--eth-motion-sync-accent-soft": content.soft,
|
|
105
|
+
...style
|
|
106
|
+
} as React.CSSProperties;
|
|
107
|
+
const busy = ariaBusy ?? (status === "syncing" ? true : undefined);
|
|
108
|
+
|
|
109
|
+
if (resolvedVariant === "compact") {
|
|
110
|
+
return (
|
|
111
|
+
<span
|
|
112
|
+
{...props}
|
|
113
|
+
aria-atomic="true"
|
|
114
|
+
aria-busy={busy}
|
|
115
|
+
aria-label={ariaLabel ?? `${labelText} ${announcement}`}
|
|
116
|
+
aria-live={ariaLive ?? (status === "syncing" ? "polite" : undefined)}
|
|
117
|
+
className={classes}
|
|
118
|
+
data-eth-component="SyncProgressAnimation"
|
|
119
|
+
data-sync-progress={progressValue}
|
|
120
|
+
data-sync-status={status}
|
|
121
|
+
role={role}
|
|
122
|
+
style={rootStyle}
|
|
123
|
+
>
|
|
124
|
+
<MotionStyles />
|
|
125
|
+
<SyncIndicator status={status} />
|
|
126
|
+
<span className="eth-motion-sync-progress__compact-copy">
|
|
127
|
+
<span className="eth-motion-sync-progress__label">{label}</span>
|
|
128
|
+
<span className="eth-motion-sync-progress__compact-state">
|
|
129
|
+
{hasProgress ? `${progressValue}%` : content.label}
|
|
130
|
+
</span>
|
|
131
|
+
</span>
|
|
132
|
+
</span>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const metaItems = [
|
|
137
|
+
{ label: "Source", value: source },
|
|
138
|
+
{ label: "Target", value: target },
|
|
139
|
+
{ label: "Rate", value: rate },
|
|
140
|
+
{ label: "Remaining", value: remaining }
|
|
141
|
+
].filter((item) => hasValue(item.value));
|
|
142
|
+
|
|
143
|
+
return (
|
|
144
|
+
<section
|
|
145
|
+
{...props}
|
|
146
|
+
aria-atomic="true"
|
|
147
|
+
aria-busy={busy}
|
|
148
|
+
aria-label={ariaLabel ?? `${labelText} ${announcement}`}
|
|
149
|
+
aria-live={ariaLive ?? (status === "syncing" ? "polite" : undefined)}
|
|
150
|
+
className={classes}
|
|
151
|
+
data-eth-component="SyncProgressAnimation"
|
|
152
|
+
data-sync-progress={progressValue}
|
|
153
|
+
data-sync-status={status}
|
|
154
|
+
role={role}
|
|
155
|
+
style={rootStyle}
|
|
156
|
+
>
|
|
157
|
+
<MotionStyles />
|
|
158
|
+
<SyncIndicator status={status} />
|
|
159
|
+
<div className="eth-motion-sync-progress__body">
|
|
160
|
+
<div className="eth-motion-sync-progress__header">
|
|
161
|
+
<div className="eth-motion-sync-progress__title-group">
|
|
162
|
+
<span className="eth-motion-sync-progress__eyebrow">
|
|
163
|
+
{stage ?? "Replication"}
|
|
164
|
+
</span>
|
|
165
|
+
<span className="eth-motion-sync-progress__label">{label}</span>
|
|
166
|
+
</div>
|
|
167
|
+
<span className="eth-motion-sync-progress__chip">
|
|
168
|
+
<span className="eth-motion-sync-progress__chip-dot" aria-hidden="true" />
|
|
169
|
+
{content.label}
|
|
170
|
+
</span>
|
|
171
|
+
</div>
|
|
172
|
+
<p className="eth-motion-sync-progress__description">{descriptionNode}</p>
|
|
173
|
+
<div className="eth-motion-sync-progress__meter">
|
|
174
|
+
<div
|
|
175
|
+
aria-label="Sync transfer progress"
|
|
176
|
+
aria-valuemax={100}
|
|
177
|
+
aria-valuemin={0}
|
|
178
|
+
aria-valuenow={progressValue}
|
|
179
|
+
aria-valuetext={hasProgress ? progressText : content.label}
|
|
180
|
+
className="eth-motion-sync-progress__track"
|
|
181
|
+
role="progressbar"
|
|
182
|
+
>
|
|
183
|
+
<span
|
|
184
|
+
className={`eth-motion-sync-progress__bar ${
|
|
185
|
+
hasProgress ? "" : "eth-motion-sync-progress__bar--indeterminate"
|
|
186
|
+
}`}
|
|
187
|
+
style={hasProgress ? { inlineSize: `${progressValue}%` } : undefined}
|
|
188
|
+
/>
|
|
189
|
+
</div>
|
|
190
|
+
<span className="eth-motion-sync-progress__progress-copy">
|
|
191
|
+
{[hasProgress ? `${progressValue}%` : content.label, countText]
|
|
192
|
+
.filter(Boolean)
|
|
193
|
+
.join(" | ")}
|
|
194
|
+
</span>
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
{metaItems.length ? (
|
|
198
|
+
<dl className="eth-motion-sync-progress__meta" aria-label="Sync metadata">
|
|
199
|
+
{metaItems.map((item) => (
|
|
200
|
+
<div className="eth-motion-sync-progress__meta-item" key={item.label}>
|
|
201
|
+
<dt>{item.label}</dt>
|
|
202
|
+
<dd>{item.value}</dd>
|
|
203
|
+
</div>
|
|
204
|
+
))}
|
|
205
|
+
</dl>
|
|
206
|
+
) : null}
|
|
207
|
+
</section>
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function SyncIndicator({ status }: { status: SyncProgressAnimationStatus }) {
|
|
212
|
+
return (
|
|
213
|
+
<span className="eth-motion-sync-progress__indicator" aria-hidden="true">
|
|
214
|
+
{status === "syncing" ? (
|
|
215
|
+
<span className="eth-motion-sync-progress__spinner" />
|
|
216
|
+
) : (
|
|
217
|
+
<span className="eth-motion-sync-progress__glyph" />
|
|
218
|
+
)}
|
|
219
|
+
</span>
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function progressFromCounts(
|
|
224
|
+
completed: number | undefined,
|
|
225
|
+
total: number | undefined,
|
|
226
|
+
status: SyncProgressAnimationStatus
|
|
227
|
+
) {
|
|
228
|
+
if (status === "synced") return 100;
|
|
229
|
+
if (!isFiniteNumber(completed) || !isFiniteNumber(total) || total <= 0) return undefined;
|
|
230
|
+
return (completed / total) * 100;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function normalizeProgress(value: number | undefined) {
|
|
234
|
+
if (!isFiniteNumber(value)) return undefined;
|
|
235
|
+
return Math.min(100, Math.max(0, Math.round(value)));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function formatCountProgress(completed: number | undefined, total: number | undefined) {
|
|
239
|
+
if (!isFiniteNumber(completed) || !isFiniteNumber(total) || total <= 0) return undefined;
|
|
240
|
+
const safeCompleted = Math.max(0, Math.floor(completed));
|
|
241
|
+
const safeTotal = Math.max(0, Math.floor(total));
|
|
242
|
+
return `${safeCompleted} of ${safeTotal} items`;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function isFiniteNumber(value: unknown): value is number {
|
|
246
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function hasValue(value: unknown) {
|
|
250
|
+
return value !== undefined && value !== null && value !== false && value !== "";
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function textFromNode(node: React.ReactNode) {
|
|
254
|
+
if (typeof node === "string" || typeof node === "number") return String(node);
|
|
255
|
+
return undefined;
|
|
256
|
+
}
|