時間の操作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
実時間

プロパティ

表は横にスクロールできます
プロパティ初期値説明
valuenumber-いまの位置。エポックミリ秒が普通ですが、フレーム番号やシミュレーションの刻みでも同じように動きます。
onValueChange(next: number) => void-飛ばしボタンやスクラブで位置が動いたときに、次の位置を返します。
nownumber-「いま」の位置。渡すと実時間かどうかの札と「いまへ戻る」が出ます。渡さなければどちらも出ません(部品は推測しません)。⚠️ 時刻は effect の中で読んでください。
liveboolean-実時間かどうかを now から導かずに直接指定します。2つの数の差では言えない「実時間」(バッファのある配信など)に使います。
liveTolerancenumber-now からどれだけ離れるまでを実時間とみなすか。既定はエポックミリ秒での1秒です。
onReturnToNow() => void-「いまへ戻る」を押したとき。省略すると now と onValueChange から onValueChange(now) が既定で使われます。
playingboolean-再生中かどうか。制御されている値です(進めるのは呼び出し側)。
onPlayingChange(playing: boolean) => void-再生・一時停止を押したとき。省略すると再生ボタンが出ません。
speedsTimeTransportSpeed[]-速さの段。{ value, label } の配列で、名前は呼び出し側がつけます。省略すると速さの選択が出ません。
speed / onSpeedChangenumber / (speed: number) => void-選ばれている速さと、その変更。speeds[].value と突き合わせます。
jumpsTimeTransportJump[]-決まった幅の飛ばし。offset は符号つきで、負が戻る方向です。負は左・正は右に並び、向きはアイコンが示します(ラベルは大きさだけ)。省略すると出ません。
formatValue(value: number) => ReactNode-大きい表示の書式。既定は String(value) なので、時刻として出すなら渡してください。
secondaryReactNode-表示の下の2行目(日付・タイムゾーン・経過など)。
scrubberReactNode-掴んで動かす面のスロット。表示と操作段のあいだに全幅で入ります(いちにちの帯はここに入ります)。
labelsTimeTransportLabels-組み込みの文字列の差し替え(group / play / pause / speed / returnToNow / live / detached / jumpBack / jumpForward)。既定は英語です。
disabledboolean-操作全体を無効にします。

使い方

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 の符号で左右に振り分け、向きはアイコンが示します。 ラベルに向きを書かせないので、そのまま多言語に出せます。
  • 実時間かどうかが分からないときは、出さない。nowlive も渡されていなければ、札も「いまへ戻る」も出ません。 推測して間違った状態を見せるより、黙っているほうが正しいためです。
  • 触れるところは 44px を割りません。再生・飛ばし・いまへ戻るは、いずれも高さ 44px です。

使用コンポーネント