いちにちの帯DayBandExperimental
1日を1本の帯にして、時間帯を面で、節目を印で、いまの時刻とつまみの位置を重ねて見せます。日の出も「いま」も呼び出し側が渡す数で、部品は1日について何も計算しません。
プレビュー
状態とバリエーション
TimeTransport の中に入れる
操作盤の scrubber スロットへ差し込んだ形。帯を掴めば時刻が動き、「いまへ戻る」で戻ります。この組み合わせのために作られた部品です。ここでの単位は「分」なので、TimeTransport の liveTolerance も分で渡しています(既定の 1000 はエポックミリ秒の1秒ぶんです)。
当番表
同じ形で当番表になります。時間帯にトーン名を渡すと、面のトークン(--*-subtle)へ解決します。節目の印は交代の時刻です。
見せるだけ(sm)
onValueChange を渡さなければ操作を受け取らず、焦点も持ちません。value も外せば、つまみのない営業時間の帯になります。size="sm" は 24px です。
掴んで流す(relative)
触っただけでは動かず、掴んだ距離ぶんだけ動きます。細い帯で指が現在位置を隠さず、誤って触っても時刻が飛びません。0時をまたぐと帯の外の値も返るので、呼び出し側が日を入れ替えます。
いちばん小さい形
時間帯だけ。印も時刻の数字も目盛りも外せます(marks を渡さず、hourLabels={[]}、tickEvery={0})。表の行の中など、狭いところに置くときの形です。
操作できない
帯全体が無効。読み取り専用の盤や、まだ日が決まっていないときに使います。
プロパティ
| プロパティ | 型 | 初期値 | 説明 |
|---|---|---|---|
| min / max | number / number | - | 1日の始まりと終わり。既定は午前0時からの分です。 |
| value | number | - | つまみが指す位置。 |
| onValueChange | (next: number) => void | - | 掴んで動かしたときの、次の位置。 |
| phases | DayBandPhase[] | - | 1日の中の時間帯。{ start, end, label?, color? } が面として敷かれます。 |
| marks | DayBandMark[] | - | 節目の印。{ at, label?, color? } で、ラベルは帯の上の行に出ます。 |
| now | number | - | 「いま」の位置。それ自身の印として描かれます。 |
| scrub | "absolute" | "relative" | - | 触った位置がそのまま時刻(absolute)か、掴んだ距離ぶん動く(relative)か。 |
| step / coarseStep | number / number | - | 矢印キーの刻みと、Shift+矢印・Page キーの刻み。既定は15分と1時間です。 |
| tickEvery | number | - | 帯の中に立てる目盛りの間隔(時間)。0 で消えます。 |
| hourLabels / formatHour | number[] / (hour: number) => ReactNode | - | 帯の下に出す時刻の数字と、その書式。[] で行ごと消えます。 |
| formatValue | (value: number) => string | - | 読み上げ(aria-valuetext)の書式。既定は「10:40 · 昼」です。 |
| size | "sm" | "default" | - | 帯の高さ。default は触れる 44px、sm は 24px です。 |
| label / hint | ReactNode / ReactNode | - | 帯の上の行。左が label、右が hint です。 |
| labels | DayBandLabels | - | 組み込みの文字列の差し替え(band / now)。既定は英語です。 |
| disabled | boolean | - | 帯を無効にし、タブ移動からも外します。 |
使い方
"use client";
import * as React from "react";
import { DayBand, TimeTransport } from "@gunjo/ui";
// 既定の単位は「午前0時からの分」(min 0 / max 1440)。
// 実日付で使うなら min にその日の0時のエポックミリ秒、max に +86400000 を渡します。
const PHASES = [
{ start: 0, end: 260, label: "夜", color: "hsl(var(--gunjo-deepest))" },
{ start: 260, end: 322, label: "薄明", color: "hsl(var(--gunjo-deep))" },
{ start: 322, end: 1076, label: "昼", color: "hsl(var(--gunjo-bright))" },
{ start: 1076, end: 1138, label: "薄明", color: "hsl(var(--gunjo-deep))" },
{ start: 1138, end: 1440, label: "夜", color: "hsl(var(--gunjo-deepest))" },
];
// 日の出・日の入は呼び出し側が渡します。部品は天文計算を持ちません。
const MARKS = [
{ at: 322, label: "日の出 05:22", color: "warning" },
{ at: 1076, label: "日の入 17:56", color: "info" },
];
function clock(minutes) {
const hh = String(Math.floor(minutes / 60)).padStart(2, "0");
const mm = String(minutes % 60).padStart(2, "0");
return `${hh}:${mm}`;
}
export function SunBand() {
const [value, setValue] = React.useState(640);
// いまの時刻を描画中に読まないこと。effect の中で読みます。
const [nowMinutes, setNowMinutes] = React.useState(undefined);
React.useEffect(() => {
const at = new Date();
setNowMinutes(at.getHours() * 60 + at.getMinutes());
}, []);
return (
<DayBand
value={value}
onValueChange={setValue}
phases={PHASES}
marks={MARKS}
now={nowMinutes}
label="東京 9月12日"
hint={clock(value)}
labels={{ band: "いちにちの中の時刻", now: "いま" }}
/>
);
}
// TimeTransport の掴んで動かす面として差し込む場合
export function BandInTransport() {
const [value, setValue] = React.useState(640);
return (
<TimeTransport
value={value}
now={640}
onValueChange={setValue}
liveTolerance={1}
formatValue={clock}
scrubber={
<DayBand value={value} now={640} onValueChange={setValue} phases={PHASES} />
}
/>
);
}設計の判断
- 時間帯の色は「面のトークン」へ解決します。
successのようなトーン名は、強い色ではなく--success-subtleの側へ 落とします。時間帯は下地で、その上に印とつまみが乗るからです。空のような絵としての色が要るときは、任意の CSS 色をそのまま渡せます。 - つまみは2色で描いています。下地の時間帯は明色でも暗色でもありうるので、背景色の線に前景色の縁を重ねています。 片方の色に賭けると、夜の帯か昼の帯のどちらかで必ず消えます。
- 印のラベルは帯の上、時刻の数字は帯の下。出どころの実装は1つの行に詰めていたため、印から48px以内の時刻の数字を消して衝突を避けていました。 行を分ければ、消す必要がありません。
- 指で触ったときは、押した瞬間には動きません。押した指がそのまま縦スクロールに変わることがあるためで、動かしたときか、指を離したとき (=はっきりした「触った」)に確定します。マウスとペンは押した瞬間です。 帯の上から始めた縦スクロールも止めません(
touch-action: pan-y)。 - 色だけに意味を載せません。読み上げには「10:40 · 昼」のように時間帯の名前が乗り、節目は文字のラベルを 持ち、時間帯にはポインタで読める名前がつきます。矢印・Shift+矢印・PageUp / PageDown・ Home / End で、触らずに1日を歩けます。
- 単位は1つだけ。
min/max/phases/marks/value/nowは、すべて同じ単位で書きます。既定は午前0時からの分 (0〜1440)で、実日付ならその日の0時のエポックミリ秒と+86400000を渡します。 部品が単位を知らないので、混ぜると黙ってずれます。 - 渡さなかったものは、出しません。
valueが無ければつまみが消えて見せるだけの帯になり、onValueChangeが無ければ操作も焦点も受け取りません。nowが無ければ「いま」の印も出ません。 - 0時をまたぐ時間帯は2つに分けます。1本の帯は1日です。
startがend以上の時間帯は描かず、開発時に 警告を出します(黙って反対側を塗るより、言うほうが正しいためです)。
使用コンポーネント
関連コンポーネント
- TimeTransport
時間を動かす操作盤。この帯はその scrubber に入る面です。
- Slider
RangeSlider と同じく値を選ぶだけ。1日の器も時間帯の面も無い。
- SegmentTimelineCard
任意の区間を段で見せるカード。こちらの器はちょうど1日。
- WeekView
ScheduleGrid と同じく日が複数のとき。1本の帯では足りない。
いつ・なぜ使うか(UIXHERO)
「いつ・なぜ使うか」の判断は、姉妹サイト UIXHERO の記事で解説しています。