Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .storybook/visual-testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ pnpm test -u # 基準画像を撮り直す(意図して見た目を変え
2. **画面が 0.8 倍に縮む。** ストーリーを描く枠(1200×900)が窓(既定 1280×720)に収まらないと、Vitest が枠ごと縮めます。縮むと 1px のずれがふちのぼかしになり、pixelmatch がふちのぼかしを数えないので見のがします。`contextOptions.viewport` を枠より大きく取りました(`browser.viewport` は provider 側で無効化されていて効きません)
3. **許す幅は割合で持たない。** `allowedMismatchedPixelRatio: 0.002` では、全ボタンの角を 12px から 0px にしても 4 本中 3 本が通りました。`allowedMismatchedPixels: 0` にしています
4. **基準画像の置き場が Storybook を壊す。** 既定の置き場は `__screenshots__/Button.stories.tsx/` で、この**ディレクトリ名**が `../src/**/*.stories.tsx` に一致し、Storybook の索引作りが EISDIR で落ちて画面が真っ白になります。`screenshotDirectory` は何を渡してもテストファイルの隣に継ぎ足されるので(絶対パスも先頭の `/` が落ちる)、Storybook 側の指定を `**` から `*` に変えて、ディレクトリに降りないようにしました
5. **`:not(:focus)` で隠す書き方は、pseudo-states で固定すると出てこない。** 「ふだんは隠れ、フォーカスしたときだけ見せる」を `not-focus:sr-only`(`:not(:focus)`)で書くと、`storybook-addon-pseudo-states` が `:not()` の中身を `:not(:focus), :not(.pseudo-focus)` に書き換えます。`.pseudo-focus` クラスを当てても `:not(:focus)` はまだ真のままなので、フォーカスした状態に固定したストーリーでも隠れたままになります。「フォーカスしたときだけ現す」ものは、`focus:` 側に見せる指定を書く形にします(SkipLink)
6. **Controls で props を切り替えても、pseudo-states は前のクラスを外さない。** `storybook-addon-pseudo-states` が要素に付けた `.pseudo-focus` などのクラスは、Controls で props を変えても残ったままで、状態の見た目が消えません。切り替えるたびに要素を作り直す(切り替える値を `key` にする)と直ります(Accordion の `indicator`・focusRing の切り替え)
7. **`pnpm test -u` は、パスを渡しても範囲を絞らないことがあります。** ディレクトリのパスだけでは全体の基準画像を撮り直すことがあります。範囲を絞るには、パスの後ろに `--update` を続けて書くか、ファイルを直接指定します(`pnpm vitest run --project=storybook <パス>` も使えます)

## 撮影の条件(`vitest.config.ts`)

Expand Down
101 changes: 95 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,27 @@

## つくりたいコンポーネント

### 土台

- [x] Icon
- [x] VisuallyHidden
- [x] ThemeProvider
- [x] Collapsible
- [x] ScrollArea
- [x] AspectRatio
- [x] Portal
- [x] Transition

### 文字

- [x] Heading
- [x] Text
- [x] Code
- [x] Kbd
- [x] Mark
- [ ] Ruby
- [ ] Highlight
- [x] NumberFormat

### 本文

Expand All @@ -37,13 +52,26 @@
- [x] Footnote
- [ ] Steps
- [ ] FileTree
- [ ] CodeGroup
- [ ] Embed
- [ ] Gallery
- [ ] ImageZoom
- [x] Bleed
- [ ] Spoiler
- [ ] TypeTable
- [ ] Mermaid
- [ ] Math

### ページの枠

- [ ] Container
- [x] Container
- [ ] Navbar
- [ ] Footer
- [ ] Sidebar
- [ ] Stack
- [x] SkipLink
- [ ] Affix
- [ ] Splitter

### ナビゲーション

Expand All @@ -54,6 +82,14 @@
- [ ] Pagination
- [ ] Menu
- [ ] Stepper
- [ ] NavigationMenu
- [ ] ContextMenu
- [ ] Toolbar
- [ ] CommandPalette
- [ ] BackToTop
- [ ] Menubar
- [ ] Tree
- [ ] Tour

### 表示

Expand All @@ -62,25 +98,37 @@
- [x] List
- [x] Table
- [x] Divider
- [ ] Time
- [x] Time
- [x] RelativeTime
- [ ] Avatar
- [x] Badge
- [ ] Chip
- [ ] Timeline
- [ ] Accordion
- [x] Accordion
- [x] Image
- [ ] AvatarGroup
- [ ] DescriptionList
- [ ] Stat
- [ ] Meter
- [ ] Carousel
- [ ] Indicator

### 操作

- [x] Button
- [x] Link
- [ ] Toggle
- [ ] ToggleGroup
- [ ] ButtonGroup
- [x] CopyButton

### 入力

- [x] TextField
- [x] Select
- [x] Switch
- [x] Form
- [ ] Textarea
- [x] Textarea
- [x] Checkbox
- [x] Radio
- [ ] Fieldset
Expand All @@ -90,22 +138,63 @@
- [ ] NumberField
- [ ] DatePicker
- [ ] Dropzone
- [ ] SearchField
- [ ] PasswordField
- [ ] Autocomplete
- [ ] PinField
- [ ] TagsInput
- [ ] TimePicker
- [ ] ColorPicker
- [ ] Rating
- [ ] Calendar
- [ ] DateRangePicker
- [x] CheckboxGroup
- [ ] MaskInput
- [ ] Editable
- [ ] FileInput

### 通知

- [x] Notice
- [x] Loading
- [ ] Toast
- [ ] Progress
- [ ] Skeleton
- [ ] Empty
- [x] Skeleton
- [ ] StatusPanel
- [ ] LoadingOverlay

### 重なるもの

- [x] Dialog
- [x] Drawer
- [x] Popover
- [x] Tooltip
- [x] AlertDialog
- [ ] PreviewCard

### アプリの画面

Web アプリで使うものです。ポートフォリオのあとに作ります。土台は外のヘッドレス(TanStack Table・TanStack Virtual・dnd-kit)に peer dependency で乗り、ここでは見た目とトークンを持ちます。

- [ ] DataTable
- [ ] VirtualList
- [ ] Sortable
- [ ] Kanban
- [ ] Popconfirm
- [ ] ActionBar
- [ ] Mentions
- [ ] Cascader
- [ ] TreeSelect
- [ ] Transfer

### チャット

- [ ] Composer
- [ ] MessageList
- [ ] Bubble
- [ ] TypingIndicator
- [ ] Attachment
- [ ] StreamingText

## Figma

Expand Down
2 changes: 1 addition & 1 deletion design/adr/0102-overlay-components.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@
- つまみを押しても、Select のシートのように半分と高さいっぱいを切り替えません。はじいたとみなす速さも Base UI の値です
- Tooltip の長押し(500ms)は実機で確かめていません。iOS の Safari で文字の選択や端末のメニューが出ないか、スクロールの始まりと取り違えないかを見ます
- 構造を固定する Provider(原則11 の「使う側が固定できる」)は、まだ部品ごとの `presentation` だけです
- AlertDialog(外を押しても Esc でも閉じない確かめ)は部品として持たず、`dismissible`・`closeOnEscape`・`closeButton` の組み合わせで作ります
- AlertDialog(外を押しても Esc でも閉じない確かめ)は部品として持たず、`dismissible`・`closeOnEscape`・`closeButton` の組み合わせで作ります(2026-09-19: この記述は [ADR-0123](./0123-alert-dialog.md) で覆しました。組み合わせを間違えないことと、読み上げの役割を確実に切り替えるため、AlertDialog を部品として作りました)

## 原則への反映

Expand Down
68 changes: 68 additions & 0 deletions design/adr/0121-reading-datetime-number.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# 0121. 文字の部品(Mark・Time・RelativeTime・NumberFormat)と、ThemeProvider の言語・タイムゾーンの既定

- ステータス: Accepted
- 日付: 2026-09-19
- ラウンド: 後半(ループ外)

## 背景

記事の外でも使える、文字まわりの小さな部品を作ります。目立たせたい言葉(Mark)は、これまで Prose が素の HTML の `<mark>` に当てる見た目だけを持ち([ADR-0091](./0091-mark.md))、部品としては存在していませんでした。日付・時刻(Time)、「3 日前」のような相対の日時(RelativeTime)、数・通貨・割合・単位(NumberFormat)は、どれも作っていませんでした。

見た目の比較ではなく、書き方(どんな文字列になるか)と、日時の既定の言語・タイムゾーンをどう持つかという設計の判断なので、比較のストーリーは作っていません。

## 候補

比較ではなく、ユーザーへの問いと答えです(ほかの軸とまとめて質問しました)。

| 問い | 答え |
| -------------------------------------- | ------------------------------------------------------------------------ |
| Time の既定の書き方 | `YYYY/MM/DD` |
| 言語・タイムゾーンの既定と、上書きの口 | 既定は ja-JP・Asia/Tokyo にしつつ、Provider などで上書きできるようにする |
| 相対の日時は別の部品にするか | 相対日付のコンポーネントとして作る |
| 通貨(JPY)の書き方 | Intl の既定でよい |

## 決定

- **Mark**: `<mark>` を描く部品にしました。見た目のクラス列は `src/internal/reading/mark.ts` に出し、Prose の `<mark>` と共有します。見た目そのものは [ADR-0091](./0091-mark.md) のままです
- **Time**: `<time dateTime>` を描き、読む文字は `Intl.DateTimeFormat` で書きます。既定は `YYYY/MM/DD`(ゼロ埋めの年/月/日。ja-JP で「2026/09/18」)です。`withTime` で時刻も付け(ja-JP で「2026/09/18 09:30」、24 時間)、`dateStyle`・`timeStyle`・`format` で Intl の書き方をそのまま渡せます。`YYYY-MM-DD` の文字列は、時刻を持たない暦の日付として扱い、どのタイムゾーンでも同じ日になるよう常に UTC で書きます。それ以外(Date・ISO 8601・ミリ秒)は `timeZone` の時刻で書きます
- **言語・タイムゾーンの既定**: `ja-JP`・`Asia/Tokyo` に固定します。ブラウザの既定の言語やタイムゾーンには従いません。サーバーで描いた文字と、閲覧者の画面で描き直した文字が食い違わないようにするためです。部品の `locale`・`timeZone` props と、ThemeProvider に足した `locale`・`timeZone` props で上書きできます(部品の props が勝ちます)
- **RelativeTime**: 「3 日前」「昨日」のように、今からの隔たりで書く別の部品にしました。今の時刻はブラウザでだけ読み、1 分ごとに書き直します。サーバーの HTML と hydration の最初の描画は、今の時刻を持たないので Time と同じふつうの日付にし、あとから相対の文字に描き直します。ふつうの日付は `title` に残します
- **NumberFormat**: `<data value>` を描き、`Intl.NumberFormat` で書きます。数・通貨・割合・単位(`unit`)を選べ、通貨の書き方(`currencyDisplay`)の既定は `symbol` です(ja-JP の JPY で「¥1,280」)。小数の桁数など、Intl が持つ既定のふるまいはそのまま使い、上書きしません
- **数字の幅**: 数字をそろえて表示する仕組み(tabular-nums 相当)は作っていません。欧文フォント(Mulish)の数字がもとから等幅で、比べても見た目の差が出なかったためです

## 理由

ユーザーの返事の原文です。

> 5: YYYY/MM/DD で

> 6: 言語の既定は ja-JP, Asia/Tokyo にしつつ、Provider などで上書きできますか?

> 7: 相対日付のコンポーネントとして作れますか?

> 8: オッケーです。

- **既定を固定する**: 6 の問いのとおり、既定は ja-JP・Asia/Tokyo に固定し、ThemeProvider から上書きできる形にしました。サーバーで生成する記事と、海外からの閲覧者がいる場面の両方を考えると、ブラウザの既定に任せるより固定するほうが表示が安定します
- **相対の日時は別部品**: 7 の問いのとおり、Time に相対表示のオプションを足すのではなく、RelativeTime という別の部品にしました。ふつうの日付と相対の日付は、サーバーとブラウザでの描き方(今の時刻を読むかどうか)が大きく違うためです
- **通貨は Intl の既定のまま**: 8 の返事のとおり、JPY の書き方を @kazuemon/ui 側で決め直すことはしませんでした

## 却下した案と理由

比較ではなく設計の判断なので、却下した案はありません。

## 影響

- `src/components/mark/Mark.tsx`・`src/components/time/Time.tsx`・`src/components/relative-time/RelativeTime.tsx`・`src/components/number-format/NumberFormat.tsx` を足しました
- `src/internal/reading/mark.ts`: Mark と Prose が共有する見た目のクラス列
- `src/internal/date/`: `format-date.ts`(日時の解釈と書き方の計算。DOM も React も使いません)・`time-text.ts`(Time・RelativeTime が共有する大きさ・濃さのクラス)・`use-locale.ts`(ThemeProvider の locale・timeZone を読む)・`use-now.ts`(RelativeTime がブラウザでだけ今の時刻を読み、1 分ごとに書き直す)
- `src/components/theme-provider/ThemeProvider.tsx`: `locale`(既定 `ja-JP`)・`timeZone`(既定 `Asia/Tokyo`)の props を足しました。内側の ThemeProvider が書いた値だけが外側より勝ちます
- `src/index.ts` に `Mark`・`MarkProps`・`Time`・`TimeProps`・`RelativeTime`・`RelativeTimeProps`・`NumberFormat`・`NumberFormatProps` を足しました
- backlog に足す未決事項: RelativeTime の「昨日」などの境界は UTC の 0 時で計算するため、端末の体感の「今日」とずれることがある。`Intl.RelativeTimeFormat`・`Intl.NumberFormat` の言い回しは ICU のバージョンでブラウザによって変わることがある。数字の幅をそろえるかは比べていない

## 原則への反映

反映なし。既存の原則の範囲内で、部品を足したものです。

## 比較画像

比較のストーリーを作っていないので、画像はありません。
59 changes: 59 additions & 0 deletions design/adr/0122-accordion.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# 0122. Accordion の既定の見た目と、項目のあいだ・一覧の上下の区切り

- ステータス: Accepted
- 日付: 2026-09-19
- ラウンド: 後半 軸96

## 背景

押して中身を開閉する項目を束ねた一覧(Accordion)を作ります。行と中身の見た目、開閉の 4 つの `appearance`(`plain`・`open-filled`・`filled`・`divided`)は [ADR-0117](./0117-collapsible.md) の Collapsible のままです。Accordion で決めるのは、既定にする `appearance` と、`divided` のときに項目のあいだだけでなく一覧の上下にも線を引くかどうかです。あわせて、同時にいくつ開けるか(`multiple`)と、題を包む見出しの段(`headingLevel`)の既定も決めます。

## 候補

| 案 | 内容 |
| ------ | ------------------------------------------------------- |
| 現行版 | `divided`・項目のあいだと一覧の上下に線を引く。FAQ の形 |
| A | `divided`・線は項目のあいだだけ。一覧の上下には引かない |
| B | `plain`。線も塗りもなく、hover だけ淡いグレー |
| C | `open-filled`。開いている項目だけグレーで塗る |
| D | `filled`。いつもグレーで塗り、項目のあいだを少し離す |

## 決定

**A(`divided`・項目のあいだの線だけ)を既定にします。** 一覧の上下には線を引きません。見出しや本文とのあいだに線が増えず、軽く見えるためです。ほかの 3 つの見た目(現行版・B・C・D)も、`appearance` の値としてどれも選べます。あわせて、`multiple` の既定は `false`(1 つ開くとほかは閉じる)にし、`headingLevel`(題を包む見出しの段)の既定は `3` にして、使う側が変えられるようにします。

## 理由

ユーザーの返事の原文です。

> 96は A デフォルトでお願いします

> 1: 1つのみデフォルト、何個でもも選択可

> 2: h3 既定、変更可

- **既定は A**: 返事のとおり A を既定にしました。一覧の上下の線をなくしたトークン(`--accordion-edge-width` 相当の切り替え)は、決めたので部品に畳んで消し、`first:border-t-0 last:border-b-0` に書き換えました
- **multiple の既定**: 「1つのみデフォルト、何個でもも選択可」のとおり、既定は 1 つだけ開く形にし、`multiple` で複数開けるようにしました
- **headingLevel の既定**: 「h3 既定、変更可」のとおり、既定を `h3` にし、ページの見出しの並びに合わせて `headingLevel` で変えられるようにしました

## 却下した案と理由

- **現行版(一覧の上下にも線)**: 既定としては選ばれませんでした。見出しや本文とのあいだに線が増え、A より重く見えます。`appearance="divided"` に一覧の上下の線を足す形自体は廃止し、比較のストーリーでは現行版の行を `className` で線を描き戻して見た目を再現しています
- **B(plain)・C(open-filled)・D(filled)**: 既定としては選ばれませんでした。どれも `appearance` の値として引き続き選べます

## 影響

- `src/components/accordion/Accordion.tsx`・`AccordionItem`: `appearance`(既定 `divided`)・`indicator`(既定 `end`)・`multiple`(既定 `false`)・`value`・`defaultValue`・`onValueChange`・`disabled`・`hiddenUntilFound`・`keepMounted`・`headingLevel`(既定 `3`)の props を持つ部品にしました。行・印・中身の見た目は Collapsible のクラス列(`src/internal/collapsible-styles.ts`。ADR-0117 で `src/components/collapsible/` から移しました)を共有します
- 一覧の上下の線を切り替えるためだけのトークンは作らず、`divided` の項目の `first:border-t-0 last:border-b-0` で表しました
- `src/index.ts` に `Accordion`・`AccordionItem`・`AccordionProps`・`AccordionItemProps`・`AccordionAppearance`・`AccordionIndicator` を足しました
- backlog に足す未決事項: 入れ子の Accordion(Accordion の中に Accordion)は確かめていません。フォームを送っているあいだ、開閉を止める仕組み(`useFormSubmittingLock` 相当)はまだありません

## 原則への反映

反映なし。見た目そのものは [ADR-0117](./0117-collapsible.md) の範囲内で、既定の選び方だけを決めたものです。

## 比較画像

比較のストーリーは、決めた時点のコミット `2f37f7f` にあります(`git checkout 2f37f7f && pnpm storybook`)。

![Accordion の既定の見た目の比較。現行版・A・B・C・D を並べたもの。A に採用の印](./assets/0122-accordion.png)
Loading
Loading