This is an automated email from the ASF dual-hosted git repository.
davidzollo pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/seatunnel-website.git
The following commit(s) were added to refs/heads/main by this push:
new 38ffb7ec9e4a [Docs] Match homepage motion states (#542)
38ffb7ec9e4a is described below
commit 38ffb7ec9e4a3ffe67135ccb6691e6f31608125e
Author: Daniel <[email protected]>
AuthorDate: Wed Sep 23 03:27:29 2026 -0400
[Docs] Match homepage motion states (#542)
---
src/components/home/MotionMechanismGrid.jsx | 357 +++++++++++++++++++++-------
src/pages/home/index.jsx | 98 ++++++--
src/pages/home/index.less | 143 ++++++++++-
3 files changed, 485 insertions(+), 113 deletions(-)
diff --git a/src/components/home/MotionMechanismGrid.jsx
b/src/components/home/MotionMechanismGrid.jsx
index 5c2025b2c5dd..a694967a111b 100644
--- a/src/components/home/MotionMechanismGrid.jsx
+++ b/src/components/home/MotionMechanismGrid.jsx
@@ -18,12 +18,16 @@
import React from 'react';
const MOTION_CYCLE_SECONDS = 8;
-const FRAME_INTERVAL_MS = 1000 / 30;
-const PARALLEL_SPLITS = Array.from({length: 6}, (_, index) => ({
- id: `S${index + 1}`,
- owner: index % 3,
- start: 0.35 + index * 0.62,
-}));
+const FOCUS_CYCLE_SECONDS = 24;
+const LOOP_TRANSITION_START_SECONDS = 7.5;
+const PARALLEL_SPLITS = [
+ {id: 'S1', owner: 2, start: 0.35},
+ {id: 'S2', owner: 0, start: 0.97},
+ {id: 'S3', owner: 1, start: 1.59},
+ {id: 'S4', owner: 2, start: 2.21},
+ {id: 'S5', owner: 0, start: 2.83},
+ {id: 'S6', owner: 1, start: 3.45},
+];
const CDC_EVENTS = [
{start: 0.4, op: 'S', id: 41, value: 'NEW'},
{start: 1.45, op: 'S', id: 42, value: 'NEW'},
@@ -36,6 +40,62 @@ const TRANSFORMS = [
{id: 0, name: 'Temp', start: 2.35},
{id: 2, name: 'Lin', start: 4.25},
];
+const DEFAULT_DIAGRAM_LABELS = {
+ barrier: 'BARRIER',
+ binlog: 'BINLOG',
+ checkpoint: 'Checkpoint',
+ checkpointState: 'Checkpoint state',
+ completed: 'COMPLETED',
+ customers: 'customers',
+ input: 'INPUT',
+ fieldId: 'id',
+ fieldStatus: 'status',
+ jdbcMysqlSink: 'JDBC MySQL sink',
+ mysqlOrders: 'MySQL / orders',
+ mysqlSource: 'MySQL source',
+ output: 'OUTPUT',
+ pending: 'PENDING',
+ reader: 'Reader',
+ readers: 'READERS',
+ route: 'route',
+ routeTables: ['users', 'orders', 'items'],
+ rowSql: 'ROW SQL',
+ sink: 'Sink',
+ snapshot: 'SNAPSHOT',
+ source: 'Source',
+ sourceSplits: 'SOURCE SPLITS',
+ table: 'Table',
+ target: 'TARGET',
+ targetOrders: 'Target / orders',
+ waiting: 'waiting…',
+ workerLost: 'Worker lost',
+ writer: 'Writer',
+ values: {
+ NEW: 'NEW',
+ PAID: 'PAID',
+ },
+ applied: 'applied',
+ filtered: 'id = 0 filtered',
+ region: 'region',
+ status: {
+ cdcDeleteArrived: 'DELETE · row 41 is removed.',
+ cdcDeleteStarted: 'DELETE · remove row 41',
+ cdcInsert: 'INSERT · new row 43',
+ cdcSnapshot: 'Read existing rows.',
+ cdcUpdate: 'UPDATE · row 42 becomes PAID',
+ checkpointBarrier: 'Capture state with a checkpoint barrier.',
+ checkpointComplete: 'Checkpoint #42 completed and retained.',
+ checkpointFailure: 'A worker fails after checkpoint #42.',
+ checkpointRestore: 'Restore saved source state from #42.',
+ checkpointRunning: 'Running · source and sink connected.',
+ checkpointResumed: 'Resumed · replay after the saved position.',
+ parallelComplete: '6 illustrated splits delivered.',
+ parallelRunning: 'Assigned splits move through 3 readers.',
+ routing: 'Table ID → matching destination writer.',
+ schema: 'schema-changes.enabled = true',
+ transform: "WHERE id > 0 · CONCAT(name, '_')",
+ },
+};
function cubicPoint(progress, path) {
const clampedProgress = Math.min(1, Math.max(0, progress));
@@ -71,49 +131,58 @@ function usePrefersReducedMotion() {
}
function useMotionClock(isPaused) {
- const [time, setTime] = React.useState(0);
- const timeRef = React.useRef(0);
+ const [clock, setClock] = React.useState({activeIndex: 0, activeProgress:
0, time: 0});
+ const elapsedTimeRef = React.useRef(0);
+ const isPausedRef = React.useRef(isPaused);
React.useEffect(() => {
- timeRef.current = time;
- }, [time]);
+ isPausedRef.current = isPaused;
+ }, [isPaused]);
React.useEffect(() => {
- if (isPaused) {
- return undefined;
- }
-
let animationFrame;
let previousFrame;
- let lastRender = 0;
const tick = (now) => {
if (previousFrame === undefined) {
previousFrame = now;
}
- timeRef.current = (timeRef.current + (now - previousFrame) / 1000)
% MOTION_CYCLE_SECONDS;
- previousFrame = now;
-
- if (now - lastRender >= FRAME_INTERVAL_MS) {
- setTime(timeRef.current);
- lastRender = now;
+ if (!isPausedRef.current) {
+ elapsedTimeRef.current += (now - previousFrame) / 1000;
+ const focusTime = elapsedTimeRef.current % FOCUS_CYCLE_SECONDS;
+ setClock({
+ activeIndex: Math.floor(focusTime / 4),
+ activeProgress: (focusTime % 4) / 4,
+ time: elapsedTimeRef.current % MOTION_CYCLE_SECONDS,
+ });
}
+ previousFrame = now;
+ // Keep the frame reference current while paused so playback
resumes without a time jump.
animationFrame = window.requestAnimationFrame(tick);
};
animationFrame = window.requestAnimationFrame(tick);
return () => window.cancelAnimationFrame(animationFrame);
- }, [isPaused]);
+ }, []);
- return time;
+ return clock;
+}
+
+function smooth(progress) {
+ const clampedProgress = Math.min(1, Math.max(0, progress));
+ return clampedProgress * clampedProgress * (3 - 2 * clampedProgress);
}
function Arrow({x, y}) {
return <path d={`M${x - 6} ${y - 5}l6 5-6 5`}
className="st-home-motion-diagram-arrow" fill="none" />;
}
+function CheckMark({x, y}) {
+ return <path d={`M${x - 5} ${y}l4 4 8-9`}
className="st-home-motion-diagram-check" fill="none" />;
+}
+
function MovingPacket({label = '', path, progress, tone = 'blue', width = 30})
{
if (progress < 0 || progress > 1) {
return null;
@@ -155,13 +224,17 @@ function cdcRows(time, isTarget) {
return [...rows.entries()];
}
-function CdcTable({x, rows}) {
+function CdcTable({labels, x, rows}) {
return (
<>
<rect x={x} y="31" width="136" height="103" rx="9"
className="st-home-motion-diagram-node" />
<rect x={x + 1} y="32" width="134" height="24" rx="7"
className="st-home-motion-diagram-soft" />
- <text x={x + 13} y="48"
className="st-home-motion-diagram-label">id</text><text x={x + 60} y="48"
className="st-home-motion-diagram-label">status</text>
- {rows.length === 0 ? <text x={x + 13} y="77"
className="st-home-motion-diagram-muted-copy">waiting…</text> : rows.map(([id,
value], index) => <g key={id}><text x={x + 13} y={77 + index * 26}
className="st-home-motion-diagram-copy">{id}</text><text x={x + 60} y={77 +
index * 26} className="st-home-motion-diagram-copy">{value}</text></g>)}
+ <text x={x + 13} y="48"
className="st-home-motion-diagram-label">{labels.fieldId}</text><text x={x +
60} y="48" className="st-home-motion-diagram-label">{labels.fieldStatus}</text>
+ {rows.length === 0 ? <text x={x + 13} y="77"
className="st-home-motion-diagram-muted-copy">{labels.waiting}</text> :
rows.map(([id, value], index) => {
+ const y = 77 + index * 26;
+ const isPaid = value === 'PAID';
+ return <g key={id}>{isPaid ? <rect x={x + 5} y={y - 17}
width="126" height="23" rx="5" className="st-home-motion-diagram-highlight" />
: null}<text x={x + 13} y={y} className={isPaid ?
'st-home-motion-diagram-teal-copy' :
'st-home-motion-diagram-copy'}>{id}</text><text x={x + 60} y={y}
className={isPaid ? 'st-home-motion-diagram-teal-copy' :
'st-home-motion-diagram-copy'}>{labels.values[value]}</text></g>;
+ })}
</>
);
}
@@ -170,42 +243,47 @@ function transformedRows(time) {
return TRANSFORMS.filter((transform) => transform.id > 0 && time >=
transform.start + 1.65);
}
-function ParallelReadsDiagram({time}) {
+function ParallelReadsDiagram({labels, time}) {
const readerYPositions = [54, 87, 120];
+ const readerTones = ['blue', 'teal', 'dark'];
+ const completedSplitCount = PARALLEL_SPLITS.filter((split) => time >=
split.start + 2.5).length;
return (
<svg viewBox="0 0 520 166" className="st-home-motion-diagram"
aria-hidden="true">
- <text x="18" y="20"
className="st-home-motion-diagram-label">SOURCE SPLITS</text>
- <text x="207" y="20"
className="st-home-motion-diagram-label">READERS</text>
- <text x="438" y="20"
className="st-home-motion-diagram-label">TARGET</text>
+ <text x="18" y="20"
className="st-home-motion-diagram-label">{labels.sourceSplits}</text>
+ <text x="207" y="20"
className="st-home-motion-diagram-label">{labels.readers}</text>
+ <text x="438" y="20"
className="st-home-motion-diagram-label">{labels.target}</text>
<rect x="16" y="32" width="118" height="112" rx="10"
className="st-home-motion-diagram-node" />
{PARALLEL_SPLITS.map((split, index) => {
const x = 28 + (index % 2) * 49;
const y = 45 + Math.floor(index / 2) * 31;
const isWaiting = time < split.start;
+ const tone = readerTones[split.owner];
return (
<g key={split.id}>
- <rect x={x} y={y} width="39" height="22" rx="5"
className={isWaiting ? 'st-home-motion-diagram-chip' :
'st-home-motion-diagram-soft'} />
- <text x={x + 19.5} y={y + 15} textAnchor="middle"
className="st-home-motion-diagram-chip-text">{split.id}</text>
+ <rect x={x} y={y} width="39" height="22" rx="5"
className={isWaiting ? 'st-home-motion-diagram-chip' :
'st-home-motion-diagram-stage'} />
+ <text x={x + 19.5} y={y + 15} textAnchor="middle"
className={isWaiting ? `st-home-motion-diagram-chip-text-${tone}` :
'st-home-motion-diagram-muted-copy'}>{split.id}</text>
</g>
);
})}
{readerYPositions.map((y, index) => (
<g key={y}>
<path d={`M134 88 C168 88 174 ${y} 204 ${y}`}
className="st-home-motion-diagram-line" fill="none" />
- <rect x="205" y={y - 13} width="121" height="26" rx="7"
className="st-home-motion-diagram-node" />
- <circle cx="220" cy={y} r="3.5" className={index === 1 ?
'st-home-motion-diagram-teal-fill' : 'st-home-motion-diagram-blue-fill'} />
- <text x="232" y={y + 5}
className="st-home-motion-diagram-copy">Reader {index + 1}</text>
+ <rect x="205" y={y - 13} width="121" height="26" rx="7"
className={`st-home-motion-diagram-node
st-home-motion-diagram-reader-${readerTones[index]}`} />
+ <circle cx="220" cy={y} r="3.5"
className={`st-home-motion-diagram-${readerTones[index]}-fill`} />
+ <text x="232" y={y + 5}
className="st-home-motion-diagram-copy">{labels.reader} {index + 1}</text>
<path d={`M326 ${y} C364 ${y} 384 88 428 88`}
className="st-home-motion-diagram-line" fill="none" />
</g>
))}
<Arrow x="427" y="88" />
- <ellipse cx="466" cy="67" rx="27" ry="7"
className="st-home-motion-diagram-node" />
- <path d="M439 67v43c0 9 54 9 54 0V67"
className="st-home-motion-diagram-node" />
+ <ellipse cx="466" cy="67" rx="27" ry="7"
className="st-home-motion-diagram-database" />
+ <path d="M439 67v43c0 9 54 9 54 0V67"
className="st-home-motion-diagram-database" />
<path d="M439 89c0 9 54 9 54 0M439 110c0 9 54 9 54 0"
className="st-home-motion-diagram-line" fill="none" />
+ <rect x="431" y="139" width="70" height="18" rx="9"
className="st-home-motion-diagram-soft" />
+ <text x="466" y="152" textAnchor="middle"
className="st-home-motion-diagram-accent-text">{completedSplitCount} / 6</text>
{PARALLEL_SPLITS.map((split) => {
const readerY = readerYPositions[split.owner];
- const tone = split.owner === 1 ? 'teal' : 'blue';
+ const tone = readerTones[split.owner];
return (
<React.Fragment key={split.id}>
<MovingPacket label={split.id} path={[134, 88, 168,
88, 174, readerY, 204, readerY]} progress={(time - split.start) / 1.25}
tone={tone} width={32} />
@@ -217,21 +295,21 @@ function ParallelReadsDiagram({time}) {
);
}
-function SnapshotCdcDiagram({time}) {
+function SnapshotCdcDiagram({labels, time}) {
const sourceRows = cdcRows(time, false);
const targetRows = cdcRows(time, true);
return (
<svg viewBox="0 0 520 166" className="st-home-motion-diagram"
aria-hidden="true">
- <text x="18" y="20" className="st-home-motion-diagram-label">MYSQL
/ ORDERS</text>
- <text x="373" y="20"
className="st-home-motion-diagram-label">TARGET / ORDERS</text>
- <CdcTable x={16} rows={sourceRows} />
+ <text x="18" y="20"
className="st-home-motion-diagram-label">{labels.mysqlOrders}</text>
+ <text x="373" y="20"
className="st-home-motion-diagram-label">{labels.targetOrders}</text>
+ <CdcTable labels={labels} x={16} rows={sourceRows} />
<rect x="201" y="24" width="98" height="23" rx="12"
className="st-home-motion-diagram-soft" />
- <text x="250" y="40" textAnchor="middle"
className="st-home-motion-diagram-accent-text">{time < 2.8 ? 'SNAPSHOT' :
'BINLOG'}</text>
+ <text x="250" y="40" textAnchor="middle"
className="st-home-motion-diagram-accent-text">{time < 2.8 ? labels.snapshot :
labels.binlog}</text>
<path d="M153 82H367" className="st-home-motion-diagram-line"
fill="none" />
<Arrow x="367" y="82" />
<text x="260" y="116" textAnchor="middle"
className="st-home-motion-diagram-copy">SeaTunnel</text>
- <CdcTable x={370} rows={targetRows} />
+ <CdcTable labels={labels} x={370} rows={targetRows} />
{CDC_EVENTS.map((event) => (
<MovingPacket key={`${event.op}-${event.id}-${event.start}`}
label={`${event.op}:${event.id}`} path={[153, 82, 210, 82, 310, 82, 367, 82]}
progress={(time - event.start) / 0.9} tone={event.op === 'S' || event.op ===
'I' ? 'teal' : 'blue'} width={43} />
))}
@@ -239,50 +317,52 @@ function SnapshotCdcDiagram({time}) {
);
}
-function TransformDiagram({time}) {
+function TransformDiagram({labels, time}) {
const outputRows = transformedRows(time);
+ const isFiltered = time >= 3;
return (
<svg viewBox="0 0 520 166" className="st-home-motion-diagram"
aria-hidden="true">
- <text x="18" y="20"
className="st-home-motion-diagram-label">INPUT</text>
- <text x="218" y="20" className="st-home-motion-diagram-label">ROW
SQL</text>
- <text x="421" y="20"
className="st-home-motion-diagram-label">OUTPUT</text>
+ <text x="18" y="20"
className="st-home-motion-diagram-label">{labels.input}</text>
+ <text x="218" y="20"
className="st-home-motion-diagram-label">{labels.rowSql}</text>
+ <text x="421" y="20"
className="st-home-motion-diagram-label">{labels.output}</text>
<rect x="16" y="31" width="125" height="91" rx="9"
className="st-home-motion-diagram-node" />
<text x="28" y="57"
className="st-home-motion-diagram-copy">1 Ada</text>
- <text x="28" y="82"
className="st-home-motion-diagram-copy">0 Temp</text>
+ <text x="28" y="82"
className="st-home-motion-diagram-muted-copy">0 Temp</text>
<text x="28" y="107"
className="st-home-motion-diagram-copy">2 Lin</text>
<path d="M142 76H206M315 76h84"
className="st-home-motion-diagram-line" fill="none" />
<Arrow x="399" y="76" />
<rect x="207" y="48" width="108" height="56" rx="13"
className="st-home-motion-diagram-blue" />
- <text x="261" y="82" textAnchor="middle"
className="st-home-motion-diagram-on-blue st-home-motion-diagram-sql">SQL</text>
+ <text x="261" y="82" textAnchor="middle"
className="st-home-motion-diagram-on-blue st-home-motion-diagram-sql">Sql</text>
<rect x="402" y="31" width="102" height="91" rx="9"
className="st-home-motion-diagram-node" />
- {outputRows.length === 0 ? <text x="413" y="77"
className="st-home-motion-diagram-muted-copy">waiting…</text> :
outputRows.map((row, index) => <text key={row.id} x="413" y={60 + index * 27}
className="st-home-motion-diagram-teal-copy">{row.id}
{row.name}_</text>)}
- <text x="18" y="144"
className="st-home-motion-diagram-code">CONCAT(name, '_')</text>
+ {outputRows.length === 0 ? <text x="413" y="77"
className="st-home-motion-diagram-muted-copy">{labels.waiting}</text> :
outputRows.map((row, index) => <text key={row.id} x="413" y={60 + index * 27}
className="st-home-motion-diagram-teal-copy">{row.id}
{row.name}_</text>)}
+ {isFiltered ? <><circle cx="261" cy="144" r="7"
className="st-home-motion-diagram-filter-icon" /><path d="M257 148l8-8"
className="st-home-motion-diagram-filter-mark" fill="none" /><text x="278"
y="148"
className="st-home-motion-diagram-muted-copy">{labels.filtered}</text></> :
<text x="18" y="144" className="st-home-motion-diagram-code">CONCAT(name,
'_')</text>}
{TRANSFORMS.map((transform) => (
<React.Fragment key={transform.id}>
<MovingPacket label={String(transform.id)} path={[142, 76,
164, 76, 184, 76, 206, 76]} progress={(time - transform.start) / 0.65}
width={22} />
- {transform.id > 0 ? <MovingPacket
label={String(transform.id)} path={[315, 76, 344, 76, 371, 76, 399, 76]}
progress={(time - transform.start - 0.85) / 0.8} tone="teal" width={22} /> :
<MovingPacket label="0" path={[261, 105, 261, 117, 261, 126, 261, 136]}
progress={(time - transform.start - 0.65) / 0.7} width={22} />}
+ {transform.id > 0 ? <MovingPacket
label={String(transform.id)} path={[315, 76, 344, 76, 371, 76, 399, 76]}
progress={(time - transform.start - 0.85) / 0.8} tone="teal" width={22} /> :
<MovingPacket label="0" path={[261, 105, 261, 117, 261, 126, 261, 136]}
progress={(time - transform.start - 0.65) / 0.7} tone="dark" width={22} />}
</React.Fragment>
))}
</svg>
);
}
-function RoutingDiagram({time}) {
- const rows = ['users', 'orders', 'items'];
+function RoutingDiagram({labels, time}) {
+ const rows = labels.routeTables;
+ const rowTones = ['blue', 'teal', 'dark'];
return (
<svg viewBox="0 0 520 166" className="st-home-motion-diagram"
aria-hidden="true">
{rows.map((table, index) => {
const y = 39 + index * 43;
- const tone = index === 1 ? 'teal' : 'blue';
+ const tone = rowTones[index];
const sourcePath = [144, y + 15, 179, y + 15, 189, 82, 221,
82];
const targetPath = [299, 82, 333, 82, 343, y + 15, 376, y +
15];
return (
<g key={table}>
<rect x="16" y={y} width="127" height="30" rx="7"
className="st-home-motion-diagram-node" />
<rect x="377" y={y} width="127" height="30" rx="7"
className="st-home-motion-diagram-node" />
- <rect x="28" y={y + 8} width="16" height="14" rx="2"
className="st-home-motion-diagram-table" />
- <rect x="389" y={y + 8} width="16" height="14" rx="2"
className="st-home-motion-diagram-table" />
+ <rect x="28" y={y + 8} width="16" height="14" rx="2"
className={`st-home-motion-diagram-table st-home-motion-diagram-table-${tone}`}
/>
+ <rect x="389" y={y + 8} width="16" height="14" rx="2"
className={`st-home-motion-diagram-table st-home-motion-diagram-table-${tone}`}
/>
<text x="55" y={y + 20}
className="st-home-motion-diagram-copy">{table}</text>
<text x="416" y={y + 20}
className="st-home-motion-diagram-copy">{table}</text>
<path d={`M144 ${y + 15} C179 ${y + 15} 189 82 221
82M299 82 C333 82 343 ${y + 15} 376 ${y + 15}`}
className="st-home-motion-diagram-line" fill="none" />
@@ -301,23 +381,23 @@ function RoutingDiagram({time}) {
);
})}
<rect x="221" y="52" width="78" height="61" rx="12"
className="st-home-motion-diagram-blue" />
- <text x="260" y="78" textAnchor="middle"
className="st-home-motion-diagram-on-blue">Table</text>
- <text x="260" y="98" textAnchor="middle"
className="st-home-motion-diagram-on-blue">route</text>
+ <text x="260" y="78" textAnchor="middle"
className="st-home-motion-diagram-on-blue">{labels.table}</text>
+ <text x="260" y="98" textAnchor="middle"
className="st-home-motion-diagram-on-blue">{labels.route}</text>
</svg>
);
}
-function SchemaDiagram({time}) {
+function SchemaDiagram({labels, time}) {
const rows = [['id', 'BIGINT'], ['name', 'VARCHAR'], ['region',
'VARCHAR']];
const sourceColumnCount = time >= 1.1 ? 3 : 2;
const sinkColumnCount = time >= 3.8 ? 3 : 2;
return (
<svg viewBox="0 0 520 166" className="st-home-motion-diagram"
aria-hidden="true">
- <text x="18" y="20" className="st-home-motion-diagram-label">MYSQL
SOURCE</text>
- <text x="371" y="20" className="st-home-motion-diagram-label">JDBC
MYSQL SINK</text>
+ <text x="18" y="20"
className="st-home-motion-diagram-label">{labels.mysqlSource}</text>
+ <text x="371" y="20"
className="st-home-motion-diagram-label">{labels.jdbcMysqlSink}</text>
{[16, 369].map((x) => <rect key={x} x={x} y="31" width="135"
height="104" rx="9" className="st-home-motion-diagram-node" />)}
- {[16, 369].map((x) => <g key={x}><rect x={x + 1} y="32"
width="133" height="24" rx="7" className="st-home-motion-diagram-soft" /><text
x={x + 14} y="48" className="st-home-motion-diagram-copy">▦
customers</text></g>)}
+ {[16, 369].map((x) => <g key={x}><rect x={x + 1} y="32"
width="133" height="24" rx="7" className="st-home-motion-diagram-soft" /><text
x={x + 14} y="48" className="st-home-motion-diagram-copy">▦
{labels.customers}</text></g>)}
{rows.map(([name, type], index) => {
const y = 76 + index * 24;
return (
@@ -333,11 +413,14 @@ function SchemaDiagram({time}) {
<rect x="225" y="48" width="58" height="22" rx="11"
className="st-home-motion-diagram-soft" /><text x="254" y="64"
textAnchor="middle" className="st-home-motion-diagram-accent-text">DDL</text>
<MovingPacket label="ADD" path={[152, 82, 220, 82, 300, 82, 364,
82]} progress={(time - 1.4) / 2.4} width={36} />
<MovingPacket label="ROW" path={[152, 82, 220, 82, 300, 82, 364,
82]} progress={(time - 5) / 1.1} tone="teal" width={38} />
+ {sourceColumnCount === 3 ? <CheckMark x="144" y="25" /> : null}
+ {sinkColumnCount === 3 ? <CheckMark x="497" y="25" /> : null}
+ <text x="260" y="151" textAnchor="middle"
className={sinkColumnCount === 3 ? 'st-home-motion-diagram-teal-copy' :
'st-home-motion-diagram-muted-copy'}>{sinkColumnCount === 3 ? labels.applied :
labels.region}</text>
</svg>
);
}
-function CheckpointDiagram({time}) {
+function CheckpointDiagram({labels, time}) {
const isComplete = time >= 2.7;
const isRestoring = time >= 4.65 && time < 5.55;
const isWriterLost = time >= 3.65 && time < 4.65;
@@ -347,44 +430,145 @@ function CheckpointDiagram({time}) {
return (
<svg viewBox="0 0 520 166" className="st-home-motion-diagram"
aria-hidden="true">
<path d="M122 70h70M327 70h70"
className="st-home-motion-diagram-line" fill="none" /><Arrow x="192" y="70"
/><Arrow x="397" y="70" />
- {['Source', 'Writer', 'Sink'].map((label, index) => {
+ {[labels.source, labels.writer, labels.sink].map((label, index) =>
{
const x = [16, 198, 403][index];
- const isLostWriter = label === 'Writer' && isWriterLost;
- return <g key={label}><rect x={x} y="47" width="104"
height="43" rx="10" className={isLostWriter ? 'st-home-motion-diagram-blue' :
'st-home-motion-diagram-node'} /><text x={x + 52} y="74" textAnchor="middle"
className={isLostWriter ? 'st-home-motion-diagram-on-blue' :
'st-home-motion-diagram-strong'}>{isLostWriter ? 'Worker lost' :
label}</text></g>;
+ const isLostWriter = index === 1 && isWriterLost;
+ return <g key={label}><rect x={x} y="47" width="104"
height="43" rx="10" className={isLostWriter ? 'st-home-motion-diagram-blue' :
'st-home-motion-diagram-node'} /><text x={x + 52} y="74" textAnchor="middle"
className={isLostWriter ? 'st-home-motion-diagram-on-blue' :
'st-home-motion-diagram-strong'}>{isLostWriter ? labels.workerLost :
label}</text></g>;
})}
<path d="M68 91C68 130 157 137 215 137M250 91v25"
className="st-home-motion-diagram-line" fill="none" />
<rect x="215" y="114" width="134" height="38" rx="9"
className={isComplete ? 'st-home-motion-diagram-soft' :
'st-home-motion-diagram-node'} />
- <text x="282" y="135" textAnchor="middle"
className="st-home-motion-diagram-copy">{isComplete ? 'Checkpoint #42' :
'Checkpoint state'}</text>
- <text x="282" y="147" textAnchor="middle" className={isComplete ?
'st-home-motion-diagram-teal-copy' :
'st-home-motion-diagram-label'}>{isComplete ? 'COMPLETED' : 'PENDING'}</text>
+ <text x="282" y="135" textAnchor="middle"
className="st-home-motion-diagram-copy">{isComplete ? `${labels.checkpoint}
#42` : labels.checkpointState}</text>
+ <text x="282" y="147" textAnchor="middle" className={isComplete ?
'st-home-motion-diagram-teal-copy' :
'st-home-motion-diagram-label'}>{isComplete ? labels.completed :
labels.pending}</text>
{showSignals ? <>
{[0, 1].map((offset) => <React.Fragment key={offset}>
<MovingPacket path={[122, 70, 145, 70, 170, 70, 192, 70]}
progress={signalProgress - offset * 0.48} tone="teal" />
<MovingPacket path={[327, 70, 350, 70, 375, 70, 397, 70]}
progress={signalProgress - offset * 0.48 + 0.2} tone="teal" />
</React.Fragment>)}
</> : null}
- <MovingPacket label="BARRIER" path={[93, 30, 225, 30, 350, 30,
488, 30]} progress={(time - 1.1) / 1.6} width={54} />
+ {time >= 1.1 && time < 2.7 ? (() => {
+ const barrierX = 93 + (488 - 93) * ((time - 1.1) / 1.6);
+ return <g><path d={`M${barrierX} 20V112`}
className="st-home-motion-diagram-barrier" fill="none" /><rect x={barrierX -
29} y="4" width="58" height="18" rx="9" className="st-home-motion-diagram-soft"
/><text x={barrierX} y="17" textAnchor="middle"
className="st-home-motion-diagram-accent-text">{labels.barrier}</text></g>;
+ })() : null}
{isRestoring ? <MovingPacket path={[215, 137, 165, 137, 112, 121,
68, 91]} progress={(time - 4.65) / 0.9} /> : null}
+ {isComplete ? <CheckMark x="338" y="129" /> : null}
</svg>
);
}
-function MotionDiagram({type, time}) {
+function renderMotionDiagram(labels, type, time) {
const diagrams = {
- parallel: <ParallelReadsDiagram time={time} />,
- cdc: <SnapshotCdcDiagram time={time} />,
- transform: <TransformDiagram time={time} />,
- routing: <RoutingDiagram time={time} />,
- schema: <SchemaDiagram time={time} />,
- checkpoint: <CheckpointDiagram time={time} />,
+ parallel: <ParallelReadsDiagram labels={labels} time={time} />,
+ cdc: <SnapshotCdcDiagram labels={labels} time={time} />,
+ transform: <TransformDiagram labels={labels} time={time} />,
+ routing: <RoutingDiagram labels={labels} time={time} />,
+ schema: <SchemaDiagram labels={labels} time={time} />,
+ checkpoint: <CheckpointDiagram labels={labels} time={time} />,
};
return diagrams[type];
}
-export default function MotionMechanismGrid({caption, controls, gridLabel,
mechanisms}) {
+function MotionDiagram({labels, type, time}) {
+ if (time < LOOP_TRANSITION_START_SECONDS) {
+ return renderMotionDiagram(labels, type, time);
+ }
+
+ const blend = smooth((time - LOOP_TRANSITION_START_SECONDS) /
(MOTION_CYCLE_SECONDS - LOOP_TRANSITION_START_SECONDS));
+ return (
+ <div className="st-home-motion-diagram-transition">
+ <div className="st-home-motion-diagram-transition-frame"
style={{opacity: 1 - blend}}>{renderMotionDiagram(labels, type, 7.49)}</div>
+ <div className="st-home-motion-diagram-transition-frame"
style={{opacity: blend}}>{renderMotionDiagram(labels, type, 0)}</div>
+ </div>
+ );
+}
+
+function getMotionStatus(labels, time, type) {
+ let status = labels.status[type];
+ let tone = 'blue';
+
+ if (type === 'parallel') {
+ const isComplete = PARALLEL_SPLITS.every((split) => time >=
split.start + 2.5);
+ status = isComplete ? labels.status.parallelComplete :
labels.status.parallelRunning;
+ tone = isComplete ? 'teal' : tone;
+ } else if (type === 'cdc') {
+ if (time < 2.8) {
+ status = labels.status.cdcSnapshot;
+ } else if (time < 4.1) {
+ status = labels.status.cdcInsert;
+ } else if (time < 5.55) {
+ status = labels.status.cdcUpdate;
+ } else if (time < 6.45) {
+ status = labels.status.cdcDeleteStarted;
+ } else {
+ status = labels.status.cdcDeleteArrived;
+ tone = 'teal';
+ }
+ } else if (type === 'routing' || type === 'schema' || type ===
'transform') {
+ status = labels.status[type];
+ tone = type === 'schema' && time >= 3.8 ? 'teal' : tone;
+ } else if (type === 'checkpoint') {
+ if (time < 1.1) {
+ status = labels.status.checkpointRunning;
+ } else if (time < 2.7) {
+ status = labels.status.checkpointBarrier;
+ } else if (time < 3.65) {
+ status = labels.status.checkpointComplete;
+ } else if (time < 4.65) {
+ status = labels.status.checkpointFailure;
+ } else if (time < 5.55) {
+ status = labels.status.checkpointRestore;
+ } else {
+ status = labels.status.checkpointResumed;
+ tone = 'teal';
+ }
+ }
+
+ return {status, tone};
+}
+
+function MotionStatus({labels, time, type}) {
+ const renderStatus = (frameTime) => {
+ const {status, tone} = getMotionStatus(labels, frameTime, type);
+ return <p className={`st-home-motion-card-detail
st-home-motion-card-detail-${tone}`}>{status}</p>;
+ };
+
+ if (time < LOOP_TRANSITION_START_SECONDS) {
+ return renderStatus(time);
+ }
+
+ const blend = smooth((time - LOOP_TRANSITION_START_SECONDS) /
(MOTION_CYCLE_SECONDS - LOOP_TRANSITION_START_SECONDS));
+ return (
+ <div className="st-home-motion-status-transition">
+ <div style={{opacity: 1 - blend}}>{renderStatus(7.49)}</div>
+ <div className="st-home-motion-status-transition-frame"
style={{opacity: blend}}>{renderStatus(0)}</div>
+ </div>
+ );
+}
+
+export default function MotionMechanismGrid({caption, controls, gridLabel,
labels = DEFAULT_DIAGRAM_LABELS, mechanisms}) {
const [isMotionPaused, setIsMotionPaused] = React.useState(false);
const prefersReducedMotion = usePrefersReducedMotion();
- const time = useMotionClock(isMotionPaused || prefersReducedMotion);
+ const {activeIndex, activeProgress, time} = useMotionClock(isMotionPaused);
+
+ React.useEffect(() => {
+ if (prefersReducedMotion) {
+ setIsMotionPaused(true);
+ }
+ }, [prefersReducedMotion]);
+
+ React.useEffect(() => {
+ const onKeyDown = (event) => {
+ if (event.code !== 'Space' || ['INPUT', 'SELECT',
'TEXTAREA'].includes(event.target.tagName)) {
+ return;
+ }
+
+ event.preventDefault();
+ setIsMotionPaused((paused) => !paused);
+ };
+
+ window.addEventListener('keydown', onKeyDown);
+ return () => window.removeEventListener('keydown', onKeyDown);
+ }, []);
return (
<>
@@ -393,7 +577,7 @@ export default function MotionMechanismGrid({caption,
controls, gridLabel, mecha
type="button"
className="st-home-motion-control"
aria-pressed={isMotionPaused}
- disabled={prefersReducedMotion}
+ aria-keyshortcuts="Space"
onClick={() => setIsMotionPaused((paused) => !paused)}
>
{isMotionPaused ? controls.play : controls.pause}
@@ -401,7 +585,11 @@ export default function MotionMechanismGrid({caption,
controls, gridLabel, mecha
</div>
<ul className="st-home-motion-grid" aria-label={gridLabel}>
{mechanisms.map((mechanism, index) => (
- <li key={mechanism.title} className={`st-home-motion-card
st-home-motion-card-${mechanism.diagram}`}>
+ <li
+ key={mechanism.title}
+ className={`st-home-motion-card
st-home-motion-card-${mechanism.diagram}${index === activeIndex ? ' is-active'
: ''}`}
+ style={index === activeIndex ?
{'--motion-active-opacity': 0.48 + 0.42 * Math.sin(Math.PI * activeProgress),
'--motion-active-progress': activeProgress} : undefined}
+ >
<article>
<div className="st-home-motion-card-heading">
<span>{String(index + 1).padStart(2,
'0')}</span>
@@ -410,9 +598,10 @@ export default function MotionMechanismGrid({caption,
controls, gridLabel, mecha
<p>{mechanism.description}</p>
</div>
</div>
- <div
className="st-home-motion-visual"><MotionDiagram type={mechanism.diagram}
time={time} /></div>
- <p
className="st-home-motion-card-detail">{mechanism.detail}</p>
+ <div
className="st-home-motion-visual"><MotionDiagram labels={labels}
type={mechanism.diagram} time={time} /></div>
+ <MotionStatus labels={labels} time={time}
type={mechanism.diagram} />
<p
className="st-home-motion-card-note">{mechanism.note}</p>
+ {index === activeIndex ? <span
className="st-home-motion-card-progress" aria-hidden="true" /> : null}
</article>
</li>
))}
diff --git a/src/pages/home/index.jsx b/src/pages/home/index.jsx
index 32868dfb5088..bf31abffc18e 100644
--- a/src/pages/home/index.jsx
+++ b/src/pages/home/index.jsx
@@ -262,7 +262,7 @@ const HOME_COPY = {
},
motion: {
title: 'Data integration, in motion.',
- lead: 'Six mechanisms that keep data moving through parallel
workloads, continuous change, and recoverable failures.',
+ lead: 'Six mechanisms behind the data integration layer.',
gridLabel: 'SeaTunnel data integration mechanisms',
controls: {
label: 'Motion controls',
@@ -271,12 +271,12 @@ const HOME_COPY = {
},
caption: 'Illustrative flows—not a deployment blueprint.
Production behavior depends on the selected connector, engine, and
configuration.',
mechanisms: [
- {diagram: 'parallel', title: 'Parallel batch reads',
description: 'Divide the work. Read independent splits in parallel.', detail:
'Independent readers process assigned source splits.', note: 'JDBC example ·
Split assignment is source-specific.'},
- {diagram: 'cdc', title: 'Snapshot + CDC', description:
'Bootstrap a snapshot, then keep changes flowing.', detail: 'Capture existing
rows before streaming changes.', note: 'MySQL-CDC example · startup.mode =
initial.'},
- {diagram: 'transform', title: 'Row-level transforms',
description: 'Shape, filter, and enrich records before they land.', detail:
'Apply row expressions and filters in the pipeline.', note: 'SQL example ·
expression + row filter.'},
- {diagram: 'routing', title: 'Multi-table routing',
description: 'Keep every record on its intended table path.', detail: 'Table
identity selects the matching destination writer.', note: 'Logical routing ·
Physical connections may differ.'},
- {diagram: 'schema', title: 'Schema evolution', description:
'Apply supported table changes before new data moves downstream.', detail:
'Propagate an added column through the compatible path.', note: 'Zeta example ·
MySQL-CDC → JDBC MySQL.'},
- {diagram: 'checkpoint', title: 'Checkpoint recovery',
description: 'Resume from a completed checkpoint after a failure.', detail:
'Capture state with a checkpoint barrier.', note: 'Exactly-once requires
compatible source and sink support.'},
+ {diagram: 'parallel', title: 'Parallel batch reads',
description: 'Divide the work. Read splits in parallel.', detail: 'Independent
readers process assigned source splits.', note: 'JDBC example · split
assignment is source-specific.'},
+ {diagram: 'cdc', title: 'Snapshot + CDC', description:
'Snapshot first. Then capture binlog changes.', detail: 'Capture existing rows
before streaming changes.', note: 'MySQL-CDC example · startup.mode = initial'},
+ {diagram: 'transform', title: 'Row-level transforms',
description: 'Compute fields and filter individual rows.', detail: 'Apply row
expressions and filters in the pipeline.', note: 'SQL example · expression +
row filter'},
+ {diagram: 'routing', title: 'Multi-table routing',
description: 'Keep each record on the right table path.', detail: 'Table
identity selects the matching destination writer.', note: 'Logical routing ·
physical connections may differ.'},
+ {diagram: 'schema', title: 'Schema evolution', description:
'Propagate supported table changes.', detail: 'Propagate an added column
through the compatible path.', note: 'Zeta example · MySQL-CDC → JDBC MySQL'},
+ {diagram: 'checkpoint', title: 'Checkpoint recovery',
description: 'Restart from a completed checkpoint.', detail: 'Capture state
with a checkpoint barrier.', note: 'Exactly-once requires compatible source /
sink support.'},
],
},
connectors: {
@@ -527,22 +527,78 @@ const HOME_COPY = {
],
},
motion: {
- title: 'Data integration, in motion.',
- lead: 'Six mechanisms that keep data moving through parallel
workloads, continuous change, and recoverable failures.',
- gridLabel: 'SeaTunnel data integration mechanisms',
+ title: '数据集成,持续流动。',
+ lead: '数据集成层背后的六种机制。',
+ gridLabel: 'SeaTunnel 数据集成机制',
controls: {
- label: 'Motion controls',
- pause: 'Pause motion',
- play: 'Resume motion',
+ label: '动画控制',
+ pause: '暂停动画',
+ play: '继续动画',
},
- caption: 'Illustrative flows—not a deployment blueprint.
Production behavior depends on the selected connector, engine, and
configuration.',
+ labels: {
+ barrier: '屏障',
+ binlog: 'Binlog',
+ checkpoint: 'Checkpoint',
+ checkpointState: 'Checkpoint 状态',
+ completed: '已完成',
+ customers: '客户',
+ fieldId: '编号',
+ fieldStatus: '状态',
+ input: '输入',
+ jdbcMysqlSink: 'JDBC MySQL 目标端',
+ mysqlOrders: 'MySQL / 订单',
+ mysqlSource: 'MySQL 源端',
+ output: '输出',
+ pending: '等待中',
+ reader: '读取器',
+ readers: '读取器',
+ route: '路由',
+ routeTables: ['用户', '订单', '条目'],
+ rowSql: '行级 SQL',
+ sink: '目标端',
+ snapshot: '快照',
+ source: '源端',
+ sourceSplits: '源端分片',
+ table: '表',
+ target: '目标端',
+ targetOrders: '目标端 / 订单',
+ waiting: '等待中…',
+ workerLost: '写入器丢失',
+ writer: '写入器',
+ values: {
+ NEW: '新建',
+ PAID: '已支付',
+ },
+ applied: '已应用',
+ filtered: 'id = 0 已过滤',
+ region: 'region',
+ status: {
+ cdcDeleteArrived: 'DELETE · 已移除第 41 行。',
+ cdcDeleteStarted: 'DELETE · 正在移除第 41 行',
+ cdcInsert: 'INSERT · 新增第 43 行',
+ cdcSnapshot: '正在读取已有行。',
+ cdcUpdate: 'UPDATE · 第 42 行变为 PAID',
+ checkpointBarrier: '通过 checkpoint 屏障捕获状态。',
+ checkpointComplete: 'Checkpoint #42 已完成并保留。',
+ checkpointFailure: 'Checkpoint #42 完成后,一个写入器发生故障。',
+ checkpointRestore: '正在从 #42 恢复已保存的 source 状态。',
+ checkpointRunning: '运行中 · source 与 sink 已连接。',
+ checkpointResumed: '已恢复 · 从已保存的位置继续回放。',
+ parallelComplete: '6 个示例分片已全部送达。',
+ parallelRunning: '已分配的分片正穿过 3 个读取器。',
+ routing: '表标识 → 匹配的目标端写入器。',
+ schema: 'schema-changes.enabled = true',
+ transform: "WHERE id > 0 · CONCAT(name, '_')",
+ },
+ },
+ caption: '示意性流图,并非部署蓝图。生产行为取决于所选连接器、执行引擎和配置。',
mechanisms: [
- {diagram: 'parallel', title: 'Parallel batch reads',
description: 'Divide the work. Read independent splits in parallel.', detail:
'Independent readers process assigned source splits.', note: 'JDBC example ·
Split assignment is source-specific.'},
- {diagram: 'cdc', title: 'Snapshot + CDC', description:
'Bootstrap a snapshot, then keep changes flowing.', detail: 'Capture existing
rows before streaming changes.', note: 'MySQL-CDC example · startup.mode =
initial.'},
- {diagram: 'transform', title: 'Row-level transforms',
description: 'Shape, filter, and enrich records before they land.', detail:
'Apply row expressions and filters in the pipeline.', note: 'SQL example ·
expression + row filter.'},
- {diagram: 'routing', title: 'Multi-table routing',
description: 'Keep every record on its intended table path.', detail: 'Table
identity selects the matching destination writer.', note: 'Logical routing ·
Physical connections may differ.'},
- {diagram: 'schema', title: 'Schema evolution', description:
'Apply supported table changes before new data moves downstream.', detail:
'Propagate an added column through the compatible path.', note: 'Zeta example ·
MySQL-CDC → JDBC MySQL.'},
- {diagram: 'checkpoint', title: 'Checkpoint recovery',
description: 'Resume from a completed checkpoint after a failure.', detail:
'Capture state with a checkpoint barrier.', note: 'Exactly-once requires
compatible source and sink support.'},
+ {diagram: 'parallel', title: '并行批读取', description:
'拆分工作,让分片并行读取。', detail: '独立读取器处理已分配的源端分片。', note: 'JDBC 示例 · 分片分配取决于具体
source。'},
+ {diagram: 'cdc', title: '快照 + CDC', description: '先完成快照,再捕获
binlog 变更。', detail: '先读取已有行,再流式处理持续变更。', note: 'MySQL-CDC 示例 · startup.mode =
initial'},
+ {diagram: 'transform', title: '行级转换', description:
'计算字段并筛选单条记录。', detail: '在 pipeline 中应用行表达式和过滤条件。', note: 'SQL 示例 · 表达式 + 行过滤'},
+ {diagram: 'routing', title: '多表路由', description:
'让每条记录走向正确的表路径。', detail: '表标识决定匹配的目标端写入器。', note: '逻辑路由 · 物理连接可能不同。'},
+ {diagram: 'schema', title: 'Schema 演进', description:
'传播受支持的表结构变更。', detail: '沿兼容路径传播新增列。', note: 'Zeta 示例 · MySQL-CDC → JDBC
MySQL'},
+ {diagram: 'checkpoint', title: 'Checkpoint 恢复', description:
'从已完成的 checkpoint 重新开始。', detail: '通过 checkpoint 屏障捕获状态。', note: 'Exactly-once
需要兼容的 source / sink 支持。'},
],
},
connectors: {
@@ -742,7 +798,7 @@ export default function Home() {
const architectureTargetRefs = useRef([]);
const language = isBrowser
- ? (window.location.pathname.startsWith('/zh-CN/') ? 'zh-CN' : 'en')
+ ? (/^\/zh-CN(?:\/|$)/.test(window.location.pathname) ? 'zh-CN' : 'en')
: (i18n.currentLocale === 'zh-CN' ? 'zh-CN' : 'en');
const isChinese = language === 'zh-CN';
const content = HOME_COPY[language] || HOME_COPY.en;
diff --git a/src/pages/home/index.less b/src/pages/home/index.less
index ee2c34124b7d..21c414674bc5 100644
--- a/src/pages/home/index.less
+++ b/src/pages/home/index.less
@@ -975,6 +975,7 @@ html[data-theme='light'] .st-home {
--motion-line: rgba(166, 203, 232, 0.28);
--motion-soft: rgba(93, 184, 226, 0.14);
--motion-blue: #5db8e2;
+ --motion-dark: #2c6b96;
--motion-teal: #52cbbf;
--motion-on-blue: #082137;
--motion-on-teal: #061f2a;
@@ -1047,12 +1048,9 @@ html[data-theme='light'] .st-home {
outline-offset: 2px;
}
-.st-home-motion-control:disabled {
- cursor: default;
- opacity: 0.58;
-}
-
.st-home-motion-card {
+ position: relative;
+ overflow: hidden;
min-width: 0;
border: 1px solid var(--motion-line);
border-radius: 1.2rem;
@@ -1060,8 +1058,11 @@ html[data-theme='light'] .st-home {
box-shadow: 0 16px 32px rgba(0, 0, 0, 0.16);
}
-.st-home-motion-card-schema {
+.st-home-motion-card.is-active {
border-color: var(--motion-teal);
+ box-shadow:
+ 0 16px 32px rgba(0, 0, 0, 0.16),
+ 0 0 0 1px rgba(82, 203, 191, var(--motion-active-opacity));
}
.st-home-motion-card article {
@@ -1131,6 +1132,25 @@ html[data-theme='light'] .st-home {
height: auto;
}
+.st-home-motion-diagram-transition {
+ position: relative;
+ width: 100%;
+}
+
+.st-home-motion-diagram-transition-frame +
.st-home-motion-diagram-transition-frame {
+ position: absolute;
+ inset: 0;
+}
+
+.st-home-motion-status-transition {
+ position: relative;
+}
+
+.st-home-motion-status-transition-frame {
+ position: absolute;
+ inset: 0;
+}
+
.st-home-motion-diagram text {
font-family: var(--ifm-font-family-base);
}
@@ -1195,6 +1215,18 @@ html[data-theme='light'] .st-home {
fill: var(--motion-blue);
}
+.st-home-motion-diagram-chip-text-blue {
+ fill: var(--motion-blue);
+}
+
+.st-home-motion-diagram-chip-text-teal {
+ fill: var(--motion-teal);
+}
+
+.st-home-motion-diagram-chip-text-dark {
+ fill: #fff;
+}
+
.st-home-motion-diagram-sql {
font-size: 20px;
}
@@ -1205,6 +1237,16 @@ html[data-theme='light'] .st-home {
stroke-width: 1.2;
}
+.st-home-motion-diagram-stage {
+ fill: var(--motion-stage);
+}
+
+.st-home-motion-diagram-database {
+ fill: var(--motion-node);
+ stroke: var(--motion-blue);
+ stroke-width: 1.6;
+}
+
.st-home-motion-diagram-line,
.st-home-motion-diagram-arrow {
stroke: var(--motion-line);
@@ -1231,6 +1273,10 @@ html[data-theme='light'] .st-home {
fill: var(--motion-blue);
}
+.st-home-motion-diagram-dark-fill {
+ fill: var(--motion-dark);
+}
+
.st-home-motion-diagram-teal-fill {
fill: var(--motion-teal);
}
@@ -1241,6 +1287,26 @@ html[data-theme='light'] .st-home {
stroke-width: 1.4;
}
+.st-home-motion-diagram-table-teal {
+ stroke: var(--motion-teal);
+}
+
+.st-home-motion-diagram-table-dark {
+ stroke: var(--motion-dark);
+}
+
+.st-home-motion-diagram-reader-blue {
+ stroke: var(--motion-blue);
+}
+
+.st-home-motion-diagram-reader-teal {
+ stroke: var(--motion-teal);
+}
+
+.st-home-motion-diagram-reader-dark {
+ stroke: var(--motion-dark);
+}
+
.st-home-motion-diagram-highlight {
fill: var(--motion-highlight);
}
@@ -1255,12 +1321,18 @@ html[data-theme='light'] .st-home {
fill: var(--motion-teal);
}
+.st-home-motion-diagram-moving-packet-dark,
+.st-home-motion-diagram-moving-signal-dark
.st-home-motion-diagram-moving-signal {
+ fill: var(--motion-dark);
+}
+
.st-home-motion-diagram-moving-signal-core {
fill: var(--motion-node);
}
.st-home-motion-diagram-moving-packet-copy-blue,
-.st-home-motion-diagram-moving-packet-copy-teal {
+.st-home-motion-diagram-moving-packet-copy-teal,
+.st-home-motion-diagram-moving-packet-copy-dark {
font-size: 10px;
font-weight: 700;
}
@@ -1273,6 +1345,30 @@ html[data-theme='light'] .st-home {
fill: var(--motion-on-teal);
}
+.st-home-motion-diagram-moving-packet-copy-dark {
+ fill: #fff;
+}
+
+.st-home-motion-diagram-check,
+.st-home-motion-diagram-filter-mark {
+ stroke: var(--motion-teal);
+ stroke-linecap: round;
+ stroke-linejoin: round;
+ stroke-width: 2.2;
+}
+
+.st-home-motion-diagram-filter-icon {
+ fill: var(--motion-node);
+ stroke: var(--motion-blue);
+ stroke-width: 1.5;
+}
+
+.st-home-motion-diagram-barrier {
+ stroke: var(--motion-blue);
+ stroke-dasharray: 4 3;
+ stroke-width: 2.4;
+}
+
.st-home-motion-card-detail {
color: var(--motion-blue);
font-size: 0.82rem;
@@ -1280,13 +1376,21 @@ html[data-theme='light'] .st-home {
line-height: 1.45;
}
+.st-home-motion-card-detail-teal {
+ color: var(--motion-teal-text);
+}
+
.st-home-motion-card-detail::before {
display: inline-block;
margin-right: 0.45rem;
- color: var(--motion-teal);
+ color: var(--motion-blue);
content: '•';
}
+.st-home-motion-card-detail-teal::before {
+ color: var(--motion-teal);
+}
+
.st-home-motion-card-note {
margin-top: 0.55rem;
padding-top: 0.55rem;
@@ -1296,6 +1400,18 @@ html[data-theme='light'] .st-home {
line-height: 1.4;
}
+.st-home-motion-card-progress {
+ position: absolute;
+ right: 1.6rem;
+ bottom: 0;
+ left: 1.6rem;
+ height: 3px;
+ border-radius: 3px 3px 0 0;
+ background: var(--motion-teal);
+ transform: scaleX(var(--motion-active-progress));
+ transform-origin: left;
+}
+
.st-home-motion-caption {
max-width: 760px;
margin-top: 1.25rem;
@@ -2785,6 +2901,7 @@ html[data-theme='light'] .st-home .st-home-motion-section
{
--motion-line: #d7e5eb;
--motion-soft: #eaf6f7;
--motion-blue: #2469a6;
+ --motion-dark: #215f91;
--motion-teal: #30adb2;
--motion-on-blue: #fff;
--motion-on-teal: #0d3945;
@@ -2807,6 +2924,16 @@ html[data-theme='light'] .st-home .st-home-motion-card {
box-shadow: 0 14px 30px rgba(87, 122, 177, 0.1);
}
+html[data-theme='light'] .st-home .st-home-motion-card.is-active {
+ box-shadow:
+ 0 14px 30px rgba(87, 122, 177, 0.1),
+ 0 0 0 1px rgba(48, 173, 178, var(--motion-active-opacity));
+}
+
+html[data-theme='light'] .st-home .st-home-motion-diagram-chip-text-dark {
+ fill: var(--motion-dark);
+}
+
html[data-theme='light'] .st-home .st-home-architecture {
background: linear-gradient(180deg, #edf4fc 0%, #e6eef8 100%);
}