時間の操作TimeTransportExperimental
いま映している値を、名前をつけた速さの段で動かし、決まった幅で飛ばし、ひと押しで「いま」へ戻すための操作盤です。時計は持たず、進めるのは呼び出し側です。
プレビュー
この見本は書き換えて試せます(速さの段と飛ばし幅が入れ替わります)
12:00:00
2026-09-12 JST
状態とバリエーション
実時間
value が now と同じとき。札が success の色と「実時間」の文字で出て、「いまへ戻る」は押せません(もう戻っているため)。
12:00:00
2026-09-12 JST
いまから外れている
value が now から離れているとき。札が warning の色と文字に変わり、「いまへ戻る」が押せるようになります。⭐ 大きい表示の色は変えていません(下の「設計の判断」)。
07:00:00
2026-09-12 JST
再生中
再生ボタンが primary で押された状態(aria-pressed="true")になり、一時停止のアイコンに替わります。
10:00:00
2026-09-12 JST
飛ばしなし
jumps を渡さなければ飛ばしボタンの列は出ません。速さと再生だけの、いちばん小さい形です。
12:00:00
2026-09-12 JST
操作できない
全体が無効。読み取り専用の盤や、まだ再生するものが決まっていないときに使います。
12:00:00
2026-09-12 JST
プロパティ
表は横にスクロールできます
| プロパティ | 型 | 初期値 | 説明 |
|---|---|---|---|
| value | number | - | いまの位置。エポックミリ秒が普通ですが、フレーム番号やシミュレーションの刻みでも同じように動きます。 |
| onValueChange | (next: number) => void | - | 飛ばしボタンやスクラブで位置が動いたときに、次の位置を返します。 |
| now | number | - | 「いま」の位置。渡すと実時間かどうかの札と「いまへ戻る」が出ます。渡さなければどちらも出ません(部品は推測しません)。⚠️ 時刻は effect の中で読んでください。 |
| live | boolean | - | 実時間かどうかを now から導かずに直接指定します。2つの数の差では言えない「実時間」(バッファのある配信など)に使います。 |
| liveTolerance | number | - | now からどれだけ離れるまでを実時間とみなすか。既定はエポックミリ秒での1秒です。 |
| onReturnToNow | () => void | - | 「いまへ戻る」を押したとき。省略すると now と onValueChange から onValueChange(now) が既定で使われます。 |
| playing | boolean | - | 再生中かどうか。制御されている値です(進めるのは呼び出し側)。 |
| onPlayingChange | (playing: boolean) => void | - | 再生・一時停止を押したとき。省略すると再生ボタンが出ません。 |
| speeds | TimeTransportSpeed[] | - | 速さの段。{ value, label } の配列で、名前は呼び出し側がつけます。省略すると速さの選択が出ません。 |
| speed / onSpeedChange | number / (speed: number) => void | - | 選ばれている速さと、その変更。speeds[].value と突き合わせます。 |
| jumps | TimeTransportJump[] | - | 決まった幅の飛ばし。offset は符号つきで、負が戻る方向です。負は左・正は右に並び、向きはアイコンが示します(ラベルは大きさだけ)。省略すると出ません。 |
| formatValue | (value: number) => ReactNode | - | 大きい表示の書式。既定は String(value) なので、時刻として出すなら渡してください。 |
| secondary | ReactNode | - | 表示の下の2行目(日付・タイムゾーン・経過など)。 |
| scrubber | ReactNode | - | 掴んで動かす面のスロット。表示と操作段のあいだに全幅で入ります(いちにちの帯はここに入ります)。 |
| labels | TimeTransportLabels | - | 組み込みの文字列の差し替え(group / play / pause / speed / returnToNow / live / detached / jumpBack / jumpForward)。既定は英語です。 |
| disabled | boolean | - | 操作全体を無効にします。 |
使い方
import { TimeTransport } from "@gunjo/ui";
// ⭐ 時計は TimeTransport の外。進めるのは呼び出し側です。
// サーバと最初の1フレームを合わせるため、時刻は effect の中で読みます。
export function Replay() {
const [now, setNow] = React.useState(BASE);
const [value, setValue] = React.useState(BASE);
const [following, setFollowing] = React.useState(true);
const [playing, setPlaying] = React.useState(false);
const [speed, setSpeed] = React.useState(60);
React.useEffect(() => {
let last = Date.now();
const id = window.setInterval(() => {
const t = Date.now();
const elapsed = t - last;
last = t;
setNow(t);
setValue((v) => (playing ? v + elapsed * speed : following ? t : v));
}, 200);
return () => window.clearInterval(id);
}, [playing, speed, following]);
return (
<TimeTransport
value={value}
now={now}
onValueChange={(next) => { setFollowing(false); setValue(next); }}
onReturnToNow={() => { setFollowing(true); setPlaying(false); setValue(now); }}
playing={playing}
onPlayingChange={setPlaying}
// 速さの段には呼び出し側が名前をつけます。1× が1秒なのか1日なのかは
// 部品には分かりません。
speeds={[
{ value: 1, label: "1×" },
{ value: 60, label: "1分/秒" },
{ value: 3600, label: "1時間/秒" },
{ value: 86400, label: "1日/秒" },
]}
speed={speed}
onSpeedChange={setSpeed}
// offset は符号つき。向きはアイコンが示すので、ラベルは大きさだけ。
jumps={[
{ offset: -86400000, label: "1日" },
{ offset: -3600000, label: "1時間" },
{ offset: 3600000, label: "1時間" },
{ offset: 86400000, label: "1日" },
]}
formatValue={(v) => formatClock(v)}
secondary="2026-09-12 JST"
/>
);
}設計の判断
- 速さは連続値ではなく、名前のついた段。出どころの実装は 1× から 10⁷× の対数の連続値でしたが、それは天文のアプリ固有の刻みです。 部品は
speeds[]で受け取ります。1× が1秒なのか1日なのかは、部品には分かりません。 - 大きい表示の色を、実時間かどうかで変えていません。意味を示す3色は、カードの背景に文字色として置くとコントラストの基準に届きません(未解決の課題として 記録済み)。状態は札が文字で言っているので、色に頼る必要がありません。
- 向きはアイコンで、ラベルは大きさだけ。
offsetの符号で左右に振り分け、向きはアイコンが示します。 ラベルに向きを書かせないので、そのまま多言語に出せます。 - 実時間かどうかが分からないときは、出さない。
nowもliveも渡されていなければ、札も「いまへ戻る」も出ません。 推測して間違った状態を見せるより、黙っているほうが正しいためです。 - 触れるところは 44px を割りません。再生・飛ばし・いまへ戻るは、いずれも高さ 44px です。