コンテンツにスキップ

デザインシステム(packages/ui)

Overview

packages/ui(@fukuemon/ui)は、fukuemon.dev の画面が使うトークンとコンポーネントを持つ。
記事の画面と、コードを動かす画面(エディタ、ファイルのツリー、端末、プレビュー、HTTP の流れ)を、同じトークンとコンポーネントで組む。
値は fukuemon.dev が決め、アクセシビリティ、タイポグラフィ、配色、レイアウトの指標と参考に、デジタル庁デザインシステム(DADS)を使う。
トークンの層とコンポーネントの分け方は、buddy の packages/ui と同じ形にする。

Scope

持つものは次のとおり。

  • プリミティブトークン、セマンティックトークン、テーマ変数の 3 つの層
  • ライトとダークのテーマ
  • 文字、余白、角丸、影、ブレークポイント、動きの段
  • コードの色(記事のコードブロック、エディタ、端末)と、HTTP の流れの色
  • ドメインを持たない Base と Case のコンポーネント
  • Storybook のカタログ
  • デザインシステムの文書(DESIGN.md と design/ の 3 つの文書)

持たないものは次のとおり。

  • Domain のコンポーネント。サイトの帯と記事の一覧の行は apps/web の features/<機能>/ に、プレイグラウンドの画面の配置は apps/playground/src/ の関心ごとのディレクトリに置く。
  • エディタと端末のエンジン(CodeMirror、xterm.js など)の選択と結び付け。プレイグラウンドの要求の prototype で決める。packages/ui は枠とテーマだけを持つ。
  • 記事の Markdown の描画と、Mermaid の図の SVG への変換。サイトの基盤の要求で扱う。
  • 縦組み。
  • トップの立体の庭の場面の色。
    • 時間帯ごとに 20 個を超える色を使い、場面のほかでは使わないので、トークンにしない。
    • 値は apps/web/src/features/top-garden/scene/scene-colors.ts だけに書く。
    • 表は トップの水墨の門と立体の庭の Design Doc にある。
    • 例外は、場面の上端と下端に敷く霞、霞の上の字、庭の中のボタンの枠の色である。ヘッダーと庭の中のボタンを霞の上に置くので、garden- で始まる 6 つのトークンにし、コントラストのチェックで確認する。 トークンの一覧は トークンの文書 にある。

Design

トークンの層

トークンを 3 つの層に分ける。
画面とコンポーネントは、テーマ変数のユーティリティだけを書く。

層 ファイル 持つもの
プリミティブトークン src/tokens/primitive.css fukuemon.dev の値。色の階調、書体、文字の大きさ、行高
セマンティックトークン src/tokens/semantic.css、src/tokens/semantic-dark.css 役割の名前。プリミティブトークンを参照し、テーマごとに参照先を替える
テーマ変数 src/tokens/theme.css Tailwind CSS のユーティリティの名前。Tailwind のデフォルトの色、文字の大きさ、角丸、影、ブレークポイントを消す
  • 例外は、ハイコントラスト(forced-colors: active)用の forced-* のユーティリティである。システムの色(CanvasText、GrayText、SelectedItem など)は、ブラウザが決める値で、テーマごとに変わらない。そのため、プリミティブトークンを通さず theme.css に直に書く。
  • 接頭辞は、プリミティブトークンを --fd-primitive-、セマンティックトークンを --fd-color- にする。
  • テーマ変数にないクラスと、任意の値のクラス(bg-[#fff] など)は、lint(oxlint の JS プラグインの eslint-plugin-better-tailwindcss)がエラーにする。Tailwind のデフォルトを消しただけでは、テーマ変数にないクラスは CSS を出さないだけでビルドが通るためである。
  • プリミティブトークンは、セマンティックトークンとテーマ変数が参照する値だけを持つ。
  • セマンティックトークンの種類は、面、文字、枠、操作、フォーカス、状態、区別、コード、挿絵の 9 つにする。
    • 状態は、エラー、注意、成功、情報の 4 つにする。文字の色と、同じ系統の薄い面を組にする。
    • コードの種類は、下の「コードの色」にある。
    • 挿絵の種類は、色ではなく、挿絵の層(遠景、中景、近景、月)の不透明度を持つ(--fd-art-*)。挿絵は文字と面の色だけで塗り、濃さをこの値で決める。ライトは近景ほど濃く、ダークは近景ほど面の色に近い。

テーマ

ライトとダークのテーマは、セマンティックトークンの参照先を替えて作る。
プリミティブトークンとテーマ変数のファイルは、テーマごとに分けない。

  • ライトの値は semantic.css の :root に、ダークの値は semantic-dark.css に置く。semantic-dark.css は、@media (prefers-color-scheme: dark) の :root:not([data-theme="light"]) で参照先を替える。
    • ダークの値を別のファイルに置くのは、コントラストのチェックが、ライトの値だけのファイルと、ダークのブロックを分けて読むためである。
  • 訪問者は、OS の設定でテーマを選ぶ。サイトの中にテーマの切り替えを置かない。
  • Storybook は、ツールバーでテーマを切り替える。[data-theme="dark"] にもダークの値を当て、[data-theme="light"] では OS の設定がダークでもライトの値にする。Storybook だけがこの属性を使う。
    • ツールバーのデフォルトは「OS の設定」にし、サイトと同じく OS の設定に従って描く。
  • コントラストのチェックは、ライトとダークの条件ごとに流す。

色

地は生成り、文字は墨、罫は温かい灰にする。
差し色は、くすんだ緑、藍、錆の 3 つにする。主の色(primary)と内部のリンクはくすんだ緑、外部のリンク(link-external)は藍、目次の現在地とサイトのヘッダーのナビの今いるページ(current-page)は錆にする。訪問済みのリンクは、色で分けない。
状態の色は、DADS の色相のとおり、成功を緑、エラーを赤に当て、注意を錆、情報を藍にする。どの値も、生成りの地の上で選ぶ。

文字と面の組は、WCAG 2.2 の AA を満たす。
文字は 4.5:1 以上、入力欄と押せるものの枠とフォーカスの表示は 3:1 以上にする。

文字

書体は、本文にシステムフォントの 400 と 700、記事の題と見出しに Zen Old Mincho の 600、等幅に Noto Sans Mono を使う。
本文は Web フォントを読み込まない。
Web フォントは Fontsource で self-host し、外部の CDN から読まない。

  • Zen Old Mincho は、ビルドで見出しに使う字だけのファイルにする。
  • 等幅の和文の字は、BIZ UDGothic の 400 と 700 で受ける。M PLUS 1 Code と比べて、見出しの字に絞ったときのファイルが小さく(400 字で約 70 KB と約 150 KB)、Fontsource に静的なウェイトがあるためである。
  • 本文の font-family は、プリミティブトークンの --fd-primitive-font-family-sans に置く。記事の題と見出しの代替の書体も、この値にする。
  • 見出しの明朝のウェイトは、テーマ変数の font-semibold(600)で書く。
  • どの Web フォントも font-display: swap にし、和文の分割は preload しない。
  • ADR-0075: 本文をシステムフォントで描き、Web フォントを記事の題と見出しの明朝と等幅だけにする

テーマ変数には、DADS のテキストスタイルのうち使うものだけを、用途の名前で置く。

名前 DADS のスタイル 用途
caption Dns-14N-130 補足、図の説明、時刻
body-dense Dns-16N-130 表のセル、入力欄の値、木の行
body Std-16N-170 記事の本文
label Oln-16B-100 ボタン、タブ
heading-sm Std-18B-160 記事の小見出し、ダイアログの題
heading-md Std-20B-150 記事の見出し
heading-lg Std-28B-150 記事の題
code 等幅の 16px のスタイル コードブロック
code-dense 等幅の 14px のスタイル プレイグラウンドの SQL の実行計画の文字
compact 12px のスタイル コードを動かす画面のコンソールの値、時刻、出力元、行の数、下の状態の帯、HTTP の流れの通信する者と状態コード、入力欄の補足と誤りの文(ADR-0062)
label-compact 太字の 13px のスタイル コードを動かす画面の領域の題、タブ、ボタン、開閉の要約、入力欄の見出し、HTTP の本文の見出し(ADR-0062)
body-compact 13px のスタイル コードを動かす画面のファイルの木の行、入力欄の値、表のセル、メニューの項目、開閉の中身、空の状態の文、注意の枠の本文、読み込み中の文(ADR-0067、提案)
code-compact 等幅の 13px のスタイル コードを動かす画面のエディタ、端末、HTTP のメソッドと URL とヘッダと本文(ADR-0067、提案)

code と code-dense は、DADS の等幅のテキストスタイルのうち、大きさが同じものの行高を使う。
code-compact の行高は、DADS に無い 154% にし、13px の文字を約 20px(20.02px)の行に置く。
DADS の 150% では、行は 19.5px になる。

余白と区切りと重なり

  • 余白は、Tailwind のデフォルトの段(1 段 4px)で書く。ブロックの間は 8px の倍数にする。DADS は余白の段の値を決めず、使う側に任せている。
  • 区切りは、罫線と余白で示す。ヘッダーの下と表の頭の上は 2px の墨の罫、ほかは 1px の border の罫にする。
  • Link は、文中に置くかで左右の余白を分ける。
    • 文中のリンクは、左右を 6px 空ける。フォーカスの表示が外へ 4px 広がり、前後の文字に重なるためである。
    • 記事の一覧の題のように文中に置かないリンクは、isStandalone を付けて左右を空けない。見出しや帯と左端をそろえるためである。
  • 操作できる要素と重なる面の角は、2px(rounded-control)で丸める。
    • コードを動かす画面だけ、角を 3 つの段で丸める。紙の面の重なりを、角の丸めで見分けるためである(ADR-0073)。
      • ペインの紙の面は 10px(rounded-pane)、PaneTabs の帯とファイルのドロワーは 14px(rounded-sheet)にする。
      • density="compact" と size="XS" の操作できる要素は 6px(rounded-control-compact)にする。
    • コードを動かす画面の外では、角丸の名前を rounded-control の 1 つだけ使う。
    • 丸めるのは、Button、IconButton、boxed の TextField、TreeView の行、Disclosure の要約、タブの閉じるボタン、Dialog、ActionMenu、検索の候補の一覧である。
    • Switch のトラックとつまみ、Checkbox の枠も丸める。
    • ConsoleEntryList のログレベルのボタンも丸める。
    • Tabs のタブ、下罫だけの検索の欄、メニューの項目、Notice、StatusBadge、Table、コードブロックの角は丸めない。罫で示す要素と、罫で区切る面のためである。
  • 面の上に重なる面(ダイアログ、メニュー、候補の一覧)は、bg-surface-raised、1px の border-field の枠、shadow-raised の影を組にして書く。
    • 影は、ぼかさずに右下へ 4px ずらす。ぼかした影は、ダークの面の上でほぼ見えないためである。
    • 影は、重なる面だけに付ける。ボタンと入力欄には付けない。影が、重なりだけを表すためである。テーマ変数に置く影の名前は、shadow-raised の 1 つだけにする。
  • ADR-0028: 操作できる要素と重なる面の角を 2px で丸め、重なる面にずらした影を付け、罫を引く動きを筆の線にする
  • ADR-0073: コードを動かす画面を、地の上に角の丸い紙の面を重ねる見た目にし、面の角を 6〜14px で丸める
  • ブレークポイントは、DADS の設定の例の 1 つ(48rem)だけを置く。
    • コードを動かす画面の配置だけ、高さが 500px 未満の画面を custom variant の short で分ける(ADR-0072)。
    • 触る端末は、Tailwind の pointer-coarse: の variant で分ける。コードを動かす画面のコンポーネントが、押せる領域を 44px にするのに使う(ADR-0071)。
  • ADR-0071: 触る端末では、コードを動かす画面の押せる領域を 44px にし、iOS と iPadOS の入力時の拡大を viewport の指定で止める
  • ADR-0072: コードを動かす画面を、幅が 48rem 未満か高さが 500px 未満のときに狭い配置にし、高さが 500px 未満ではペインを左の縦のタブで切り替える
  • 文字の列(記事の本文、シリーズの説明、トップの Profile)の幅は、全角 40 字ぶんにする。測度の値は、テーマ変数の --container-body に置く。本文の字が 1rem なので、値も rem で決め、ブラウザの字の大きさの設定を変えても 40 字ぶんを保つ。
  • ヘッダーと本文の外枠は、旧サイトの一覧の幅の 68rem にそろえ、テーマ変数の --container-page に置く。記事の一覧は、題と日付の列を青海波の帯と同じ外枠の幅に広げ、日付を右端に寄せる。文字の列だけを、--container-body に抑える。ヘッダーのロゴの左端と本文の左端は、余白を外枠の外側に置いてそろえる。

フォーカスの表示

フォーカスの表示は、要素から 2px 離して引く、墨(focus)の 2px の 1 重の輪にし、テーマ変数のファイルの focus-ring の 1 つのユーティリティに置く。
輪の色は文字と同じ墨で、ダークのテーマでは明るい墨になる。
輪は要素の後ろの面の上に出るので、focus と面の組をコントラストのチェックに入れる。
surface とのコントラスト比は、ライトで 17.25:1、ダークで 16.28:1 である。
DADS は、要素に接する黄色の輪と、その外の黒の枠の 2 重の表示を求める。
運営者が、黄と黒は和の感じに合わないので、墨の輪にすると決めた。
1 重の輪は、輪と明るさの近い面の上では見えにくい。

コードを動かす画面の詰めたペインの中の行と FileTree の名前の欄、ペインの全体を占めるスクロールする領域は例外で、同じ墨の 2px の輪を、要素の内側に 2px 入れて引く。行は focus-ring-inset、スクロールする領域は focus-ring-frame で引く。
focus-ring-inset を使うのは、density="compact" の TreeView の項目と Tabs のタブと閉じるボタン、HttpExchangeList の行、FileTree の名前の欄である。
FileTree の名前の欄は、欄の枠に重ねて輪を引き、欄を描く TreeView の行には輪を引かない。行の輪と欄の枠が接して、二重の枠に見えるためである。
focus-ring-frame を使うのは、ペインの全体を占めるスクロールする領域で、コンソールの行の領域と、カタログの見本のエンジンが当たる。
スクロールする領域は、領域自身の輪が中の行や帯の背景に隠れて欠けるので、領域を 1 つだけ直下に置く外側の要素に focus-ring-frame を付け、その ::after に同じ輪を重ねて引く。領域自身の輪は、この要素が消す。
隙間なく並べた 24px の行とスクロールする領域では、外へ広がる輪が隣の行に掛かるか、領域の端で切れるためである。
VS Code と DevTools も、一覧の行のフォーカスを、行の内側の枠で示す。

検索の欄(TextField の appearance="underline")も例外で、標準の輪を出さず、フォーカスすると緑(primary)の筆の線を左から 1 回引く。
例外にする理由は、墨の下罫だけで組んだ欄を輪で囲むと、罫の版面が崩れるためである。
運営者が、検索の欄の和の感じを罫で出すと決めた。
緑の筆の線は、ライトで 6.55:1、ダークで 7.86:1 と、3:1 を超える。ただし、フォーカスの前の墨の罫との差は、色よりも太さ(1px の罫から、4px の筆の線)と、印の色(地味な墨から緑)が担う。キーボードの利用者の見つけやすさは、標準の輪より劣る。
動きを減らす設定では、筆の線を引く動きを止め、全長の筆の線を最初から出す。
ほかのコンポーネントには、この例外を使わない。FOCUS-ONE のチェックは、focus の変種に ring、outline、border、shadow を付ける書き方を止める。この欄は、peer-focus:visible と peer-focus:animate-stroke で書くので、チェックに当たらない。

動き

動きの時間と緩急は、Tailwind のデフォルトのユーティリティと、テーマ変数のファイルに足した animation とユーティリティで書く。
テーマ変数のファイルに足すのは、デフォルトで組めない動きだけにする。
サイト全体の動きの規則(位置を動かさない、動きのために JavaScript を配らない、5 秒を超える動きを止める)は、全体の Design Doc の「動き」 にある。

ADR-0028: 操作できる要素と重なる面の角を 2px で丸め、重なる面にずらした影を付け、罫を引く動きを筆の線にする

動きを付ける操作

動きを付ける操作は、次のものだけにする。

  • 木の行、タブ、タブの閉じるボタンの色は、transition-colors(150ms)で替わる。
  • ファイルの木と Disclosure の山形は、開閉で 90° 回る(150ms)。
  • 選んだタブの下と、検索の欄のフォーカスには、緑の筆の線を左から 1 回引く(animate-stroke と stroke-brush、320ms)。筆の線は、入りが太く、終わりを払う形である。
  • Link は、hover で下線の下に 2px の罫を左から 1 回引く(link-rule、240ms)。罫は背景で描き、文中のリンクが折り返したときは 1 行目から順に全ての行に引く。筆の線は、折り返さない所だけに使う。
    • 記事の一覧の題のように文中に置かないリンクを flex の子にして折り返したときは、罫は最後の行の下にだけ、リンクの幅で出る。flex の子の <a> はブロックの箱になり、背景は箱の下端に 1 本だけ描かれるためである。
    • 押せる領域を 44px に広げたリンク(isLargeTarget)は、下線と罫を中の文字の <span> に引き、リンクの hover で伸ばす。高さ 44px の箱に罫を描くと、罫が下線から離れて箱の下端に出るためである。
  • ナビゲーションは、折り返すかで線の形を分ける。
    • サイトのヘッダーのナビは、今いるページの印を錆の筆の線にし、ページを開くと左から 1 回引く(320ms)。hover では、墨の筆の線を左から 1 回引く(240ms)。ヘッダーのナビの文字は折り返さない。
    • Breadcrumb のリンクは、hover で、リンクの箱の下に筆の線を左から 1 回引く(brush-hover、240ms)。線は箱に付けるので、ラベルが折り返しても 1 本で崩れない。
    • TableOfContents の見出しと、SeriesPager の記事の題は折り返すので、hover で文字のすぐ下に 2px の罫を引く(link-rule、240ms)。行高 130% で折り返しても、罫は次の行の文字に接しない。
      • SeriesPager の記事の題には下線を引くので、罫は下線に重なり、hover で下線が太くなる形に見える。
    • フォーカスの表示には、ナビゲーションでも動きを付けない。
  • Button は、押している間は面を濃くし、離すと文字の色の墨がにじんで消える(ink-press、480ms 以下)。
  • ダイアログ、メニュー、検索の候補の一覧と候補が無いときの文は、濃度を上げて出る(animate-fade-in、150ms)。ダイアログは、上端に墨の筆の線を 1 回引く。
  • 横に送る枠で右に続きがある間は、右端を背景の色で薄くする(scroll-fade-end)。薄い色は、右端まで送る手前から消え始める。JavaScript を使わない。Table の枠、記事の本文の表と図とコードブロックの枠、HttpExchangeDetail の本文の枠、プレイグラウンドの SQL の実行計画の枠で使う。薄い色は surface の色で、枠が別の面の上にあるときは、--fd-scroll-fade-bg にその面の色を渡す。code-surface の上では、scroll-fade-code を並べて付ける。
  • Button の読み込み中は、3 つの点が左から順に薄くなる(loading-dots、1 周 1200ms)。Loading は、薄い罫の上を主の色の罫が伸びて抜ける(animate-loading、1 周 1600ms)。PageLoading は、同じ動きで、主の色の筆の線が伸びて抜ける(animate-loading と stroke-brush)。

動きの長さ

  • 操作に応える動き(色、濃度、山形の回転)は、150ms 以下にする。
  • 罫と筆の線を引く動きは、320ms 以下で 1 回にする。
  • 墨のにじみは、480ms 以下で消す。押した結果を待たせない飾りなので、操作に応える動きの長さの外に置く。
  • 繰り返す動きは、処理中の印(Button の読み込み中、Loading、PageLoading)だけにする。
  • スクロールに合わせた動き(scroll-fade-end)は、スクロールの位置で濃さが決まる。時間は持たない。

動きを付けない所

  • 操作の結果(切り替えた中身、開いた面の中身)は、動きの終わりを待たずに出す。動きは、結果の上に重ねる。
  • フォーカスの表示には、動きを付けない。キーボードの利用者が、今いる場所をすぐに見つけられるためである。検索の欄の筆の線だけが例外である。
  • 入力した文字と誤りの文には、動きを付けない。
  • 位置と配置を動かさない。開閉で中身が出るときも、高さを動かさない。
  • 動きのために JavaScript を配らない。
  • OS で動きを減らす設定にした訪問者には、時間で進む動きを全て止める。筆の線は全長で、処理中の印は止まった形(濃い 3 つの点、全長の罫)で出す。横に送る枠の右端の薄い色(scroll-fade-end)は、途中の濃さを出さず、右端まで送ったときに一度に消える。止め方は、テーマ変数のファイルの末尾の 1 か所に置く。

ファイルの印

FileIcon は、ファイルの名前の拡張子から、コード、文書、データ、そのほかの 4 つの系統を決める。
系統は線の形で見分け、色は差し色の役割に合わせる。コードは藍(info)、文書は緑(primary)、データは墨、そのほかは薄い墨である。
錆は目次の現在地と注意の役割なので、ファイルの印には使わない。
ディレクトリは開閉で形が変わる。印は自前の SVG で、読み上げから外し、角は丸めない。

コードの色

コードの色は、セマンティックトークンのコードの種類に置き、テーマごとに参照先を替える。
記事のコードブロック、エディタ、端末は、同じトークンから作ったテーマを使う。

トークン 当てる字句
code-surface コードの面
code-text 識別子、記号、ほかの種類に当たらない字句
code-comment コメント。斜体を重ねる
code-keyword 予約語、演算子のキーワード。太字を重ねる
code-string 文字列、正規表現
code-number 数値、真偽値、null のような定数
code-function 関数とメソッドの名前
code-type 型、クラス、HTML のタグ、JSON のキー
code-ansi-* 端末の 16 色。赤、緑、黄、青、マゼンタ、シアン、灰の階調に当てる
  • 字句の種類を増やさない。種類を増やすと、隣り合う色の差が小さくなり、見分けられなくなる。
  • どのトークンも、code-surface に対して 4.5:1 以上にする。コメントも本文と同じ基準にする。
  • 色だけで種類を分けない。コメントの斜体とキーワードの太字を、色に重ねる。
  • 選んだ行と、エディタの選択の範囲は、状態の色の薄い面を使う。

生成のスクリプトは、セマンティックトークンをテーマごとに色の値まで解決し、src/tokens/code-theme.gen.json に書き出す。
Expressive Code、エディタ、端末のテーマは、この JSON から作る。
CSS の変数を読める描画(CodeMirror のクラスで当てる色)は、CSS の変数を直に使う。

  • src/tokens/code-theme.ts の toVsCodeTheme が、JSON から VS Code の形式のテーマを作る。Expressive Code と Shiki は、このテーマを読む。
  • ダークのコードの面は地より暗い墨にし、ページの面と分ける。
  • 端末の bright の色は、ライトでは通常の色より暗い階調にする。ライトのコードの面の上で 4.5:1 を保つためである。
  • 生成のスクリプトは scripts/build-code-theme.mjs で、pnpm run check:tokens が生成物のずれを見つける。

HTTP の流れの色

HTTP の流れの表示には、新しい色のトークンを作らず、状態の色と区別の色に当てる。

表示するもの 当てるトークン
1xx のレスポンス 状態の色を当てず、StatusBadge の neutral にする
2xx のレスポンス 成功
3xx のレスポンス(リダイレクト) 情報
4xx のレスポンス 注意
5xx のレスポンスと、届かなかったリクエスト エラー
通信する者(ブラウザ、クライアント、認可サーバー、DB) 区別の色。1 つの流れの中で、者ごとに 1 色を固定する

1 つの流れに出す者は、区別の色の数と同じ 6 つまでにする。
7 つ目の者には、1 つ目の者と同じ色が当たり、色で見分けられなくなるためである。

区別の色は、隣り合う番号の色相が OKLCH で 90° 以上離れる並びにする。
最初の 3 つの色は、どの 2 つも 90° 以上離れ、最初の 4 つの色は、どの 2 つも 50° 以上離れる。
3 つか 4 つの者がやり取りする流れで、色でも者を見分けやすくするためである。
6 番と 1 番の組は、この条件の対象にしない。
5 つ以上の者の流れでは、黄土とオリーブの色相が近いので、色だけでは見分けにくい。

状態コードと者の名前は、色と一緒に文字で出す。
色を見分けられない訪問者も、どの者の間の、どの結果の通信かを読み取れるようにするためである。

コンソールのログレベルの色

コンソールのログレベルにも、新しい色のトークンを作らず、状態の色を当てる。

ログレベル 印の形 当てるトークン
debug 3 つの点 印は text-secondary。行の面は変えない
log なし(幅だけ空ける) 印を描かない。行の面は変えない
info 円の中の i 印は情報。行の面は変えない
warn 三角の中の ! 印は注意。行の面を注意の薄い面にし、左に注意の罫を引く
error 円の中の × 印はエラー。行の面をエラーの薄い面にし、左にエラーの罫を引く
  • 薄い面の行の文字は text にする。text-secondary と薄い面の組は、コントラストのチェックの対象に無いためである。
  • 値の文字は、コードの字句の色を使わず text にする。コードの字句の色は、code-surface の上でだけ 4.5:1 を確認しているためである。

コンポーネントの分類と置き場

コンポーネントは BCD Design で分類し、名前はドメイン、場面、基本の語の順に並べる。
packages/ui には Base と Case だけを置く。

分類 置き場 例
Base packages/ui/src/base/<名前>/ Button、Tabs、TreeView
Case packages/ui/src/case/<名前>/ SearchBox、FileTree、HttpExchangeList
パターン packages/ui/src/patterns/ まだ無い
Domain apps/web/src/features/<機能>/、apps/playground/src/<関心ごと>/ サイトの帯、記事の一覧の行、プレイグラウンドの画面の配置

パターンは、コンポーネントの組み合わせが 2 つ以上の画面に出たときに、src/patterns/ に置く。
src/patterns/ には、まだパターンが無い。

コンポーネントは React で書く。
クラスは clsx だけで組み、shadcn、Base UI、class-variance-authority、tailwind-merge を使わない。
見た目は variant のような props で変え、呼び出す側から className で上書きさせない。
Astro のページは、client:* を付けずに React のコンポーネントを置き、静的な HTML として描く。
操作に JavaScript が要るコンポーネントだけを、island として client:* で読み込む。

記事の画面で使うコンポーネントは、JavaScript を無効にしても内容を読めるようにする。
開閉は <details> で、タブの中身は全てのパネルを HTML に出してから JavaScript で隠す。

画面ごとのコンポーネント

記事の画面とコードを動かす画面に要るコンポーネントは、次のとおり。
DADS にないコンポーネントは、WAI-ARIA Authoring Practices Guide のパターンのキーボードの操作に従う。

両方の画面で使う Base は次のとおり。

コンポーネント 使う場所 元にするもの
Button、IconButton コードの写し、実行、ファイルの操作 DADS のボタン
Link 本文の内部と外部のリンクと、文中に置かないリンク DADS のリンク
Tabs 記事の中の例の切り替え、エディタのファイルのタブ DADS のタブ。エディタのために閉じるボタンを足す
Dialog 検索、ConfirmDialog が組む確認 DADS のモーダルダイアログ
ActionMenu ファイルとディレクトリの操作のメニュー DADS のメニューリスト
TextField 検索の語の入力 DADS のインプットテキスト
Table 記事の表、SQL の結果、HTTP のヘッダ DADS のテーブル
StatusBadge HTTP の状態コード、記事の種別 DADS のラベル
Notice 記事の注意書き、実行のエラー DADS のノティフィケーションバナー
Disclosure 記事の補足、リクエストとレスポンスの詳細 DADS のアコーディオン
Switch AI からの操作の許可のように、すぐに反映される設定 APG の Switch
Checkbox 確認の画面の「今後尋ねない」のような選択 DADS のチェックボックス
Loading 領域の中の待ち、パッケージのインストール DADS のプログレスインジケーター
PageLoading コードを動かせるようになるまでの待ち 自前(Loading の動きと筆の線)
Breadcrumb 記事とシリーズの位置 DADS のパンくずリスト
TreeView FileTree と ConsoleEntryList が組む木の表示 APG の Tree View
SplitPane エディタ、端末、プレビューの間の境界 APG の Window Splitter
FileIcon ファイルの木とエディタのタブの印 自前の SVG

記事の画面の Case は次のとおり。

コンポーネント 担うこと
SearchBox 記事の検索の語の入力と候補
TableOfContents 記事の見出しの一覧と、読んでいる見出しの表示
SeriesPager シリーズの中の前と次の記事へのリンク

コードを動かす画面の Case は次のとおり。

コンポーネント 担うこと
FileTree ファイルとディレクトリの一覧、作成、名前の変更、削除。TreeView と ActionMenu を組む
EditorFrame ファイルのタブと、エディタのエンジンと、下の状態の帯を置く枠
TerminalFrame 端末の題と、端末のエンジンを置く枠
PreviewFrame プレビューの URL の欄と、iframe を置く枠
HttpExchangeList 行き来したリクエストとレスポンスを順に並べた一覧。者、メソッド、URL、状態コードを出す
HttpExchangeDetail 1 つの行き来のヘッダ、Cookie、本文
ConsoleEntryList console.* の出力の一覧。ログレベルで絞り、値を TreeView で開いて見せる
ConfirmDialog ファイルの削除のような、データを失う操作を確定する前の確認。Dialog と Button を組む
PaneTabs 狭い配置で、ペインを 1 つずつ切り替えるタブの帯。下、左、上に置く
FileDrawer 狭い配置で、ファイルの木を画面の左から重ねて出すドロワー

コードを動かす画面の Case のうち、PaneTabs、FileDrawer、ConfirmDialog を除くものは、ToolPane の領域として描き、次の形にそろえる。

  • 領域は、地(surface-ground)の上に重ねた紙の面(surface-pane)にし、10px(rounded-pane)で丸める。四方の枠を引かず、面と地の濃さの差で区切る(ADR-0073)。
    • ハイコントラストのときだけ、面に 1px の枠を引く。
    • PaneTabs のタブの中に置いた領域は、題を読み上げだけに残す。flush の PaneTabs の中では、面の角を丸めない。
  • 題の帯は 28px(触る端末では 44px)で、題は text-compact の薄い墨にする。下の状態の帯は 24px で、text-compact にする。ペインの中の罫は、タブの帯の下だけに引く。
  • 中に置く Base は、density="compact"(Button は size="XS")で詰める(ADR-0062)。領域の読み込み中とエラーも、Loading と Notice の compact で出す。
    • 全ての領域が同じ処理を待つ間は、領域の state を pending にして題だけを出し、PageLoading で画面を覆って、中央に待ちの印を 1 つだけ出す。同じ待ちの文が領域の数だけ並ぶと、何を待っているかが分かりにくくなるためである。
      • pending の領域は、操作と、題の下の帯(ファイルのタブ、URL の欄)も出さない。待ちの間にこれらへフォーカスすると、待ちが終わって領域が描き直されたときに、フォーカスが失われるためである。
    • IconButton、ActionMenu、TextField、Button は 28px、Tabs は 32px にする。
    • TreeView の行と Disclosure の要約は 24px、Table のセルは上下 2px の余白にする。
    • 触る端末(pointer: coarse)では、上の押せる要素と木の行を 44px にする。コンポーネントが pointer-coarse: の variant で自分で替える(ADR-0071)。

カタログ

カタログは、packages/ui の Storybook にする。
ページは、はじめに、トークン、コンポーネント、パターン、画面の 5 つに分ける。

  • story は、コンポーネントの状態ごとに 1 つ置く。
  • トークンとコンポーネントのページは、story に説明の MDX を付ける。
    • MDX のページの構成は、カタログの文書(design/catalog.md)の「カタログのページ」にある。
  • MDX のページの背景と文字と、ページに iframe で埋めた story は、ツールバーで選んだテーマで描く。
    • Storybook の Docs のページは固定の色で描かれ、iframe の story はツールバーのテーマを受け取らないためである。
    • 描き方は .storybook/preview.ts と .storybook/preview.css にある。
  • 全ての story で、Storybook の a11y のアドオン(axe)を動かし、違反をテストの失敗にする。
  • カタログに載せるのは、コンポーネントとダミーデータだけにする。
  • カタログは、サイトとは別の Worker に、design.fukuemon.dev で公開する。ドメインは infra/cloudflare/ の Terraform で持ち、develop で check の workflow が通ったときに出す。

DADS から外れる点

DADS から外れる点と理由は次のとおり。

  • 色の値を fukuemon.dev が決める。地は生成り、罫は温かい灰、差し色はくすんだ緑、藍、錆である。DADS の色相の規則(成功は緑、エラーは赤)は守り、組み合わせをコントラストのチェックで確認する。運営者が、今のサイトの和の感じを残すと決めたためである。
    • 注意の色を、DADS の黄か橙ではなく、錆にする。錆は橙に近い色相で、旧サイトの差し色である。
    • 訪問済みのリンクを、色で分けない。旧サイトと同じく、内部のリンクの色を 1 つに保つ。
  • 角丸と影に、DADS のコンポーネントごとの値を使わない。区切りは罫線で示し、角は操作できる要素と重なる面だけを 2px で丸め、影は重なる面だけにぼかさずに付ける。DADS のコンポーネントの角丸と影は、罫で区切る版面と合わないためである。
    • 検索の欄は、墨の下罫だけを引く(TextField の appearance="underline")。送るボタンは置かず、Enter のキーで送る。欄が 1 つの form なので、JavaScript が無くても送れる。
    • 見出しは読み上げにだけ渡し、画面には虫眼鏡の印と、明朝の案内の文(placeholder)を出して欄の役割を示す。和の感じは、ボタンではなく欄の罫と書体で出す。
    • Loading の印は、円ではなく、薄い罫の上を主の色の罫が左から伸びて右へ抜ける形にする。版面と同じ罫で組めるためである。Button の読み込み中は、3 つの点で示す。罫は、ボタンの中では「—」に見えるためである。
  • ダークのテーマを持つ。ダークの値も fukuemon.dev が選び、組み合わせをコントラストのチェックで確認する。
    • 訪問者は、本文とコードを長く読み、暗い部屋で読むこともあるためである。
  • Tabs に閉じるボタンを足す。エディタのファイルのタブは、開いたファイルを閉じる操作が要るためである。
    • 閉じるボタンはマウスで押すためのもので、読み上げと Tab のキーの順から外す。タブの並び(role="tablist")の中に、タブ以外の操作を置けないためである。
    • キーボードと読み上げでは、選んだタブで Delete のキーを押して閉じる。閉じられるタブは aria-keyshortcuts="Delete" を持つ。
    • マウスの中ボタンのクリックでも閉じる。Ctrl+W は、ブラウザのタブを閉じるキーと衝突するので割り当てない。
  • TreeView に、APG の Tree View が必須にしない操作を 3 つ足す。
    • 文字を打つと、その文字で始まる項目へ移る(タイプアヘッド)。長い木で目的の項目へ早く移るためである。
    • onRequestRename を渡すと、項目で F2 のキーを押して名前の変更を呼べる。項目は aria-keyshortcuts="F2" を持つ。
    • onRequestDelete を渡すと、項目で Delete のキーを押して削除の確認を呼べる。項目は aria-keyshortcuts="Delete" を持つ。両方を渡すと、aria-keyshortcuts は "F2 Delete" になる。
    • 削除の確認は呼ぶ側が ConfirmDialog で行うので、キーを押しただけでは消えない。Tabs の Delete は表示を閉じる操作で、木の Delete は確認を開く操作である。キーの意味は、フォーカスのあるコンポーネントで決まる。
  • Button の見た目の種類を、primary、secondary、danger の 3 つにする。
    • DADS の文字だけのボタンは持たない。文字だけの操作は Link か IconButton で足りるためである。
    • danger は、ファイルの削除のように、データを失う操作を確定するボタンに使う。DADS のボタンには、この種類がない。
  • コードの色を持つ。DADS は、シンタックスハイライトの色を決めていない。
  • 記事の題と見出しの書体を、Zen Old Mincho の 600 にする。heading-* のスタイルは、DADS の大きさと行高を使い、書体とウェイトだけを替える。運営者が、今のサイトの和の感じを残すと決めたためである。
  • 本文の書体を、DADS の Noto Sans JP からシステムフォントにする。本文の Web フォントの取得をなくし、トップの Performance を 90 に届かせるためである(ADR-0075)。

Interface

置くファイルは次のとおり。

パス 持つもの
packages/ui/DESIGN.md 画面を作るときの原則と手順、アクセシビリティ
packages/ui/design/tokens.md トークンの使う場面
packages/ui/design/components.md コンポーネントの選び方と、コンポーネントごとの規則
packages/ui/design/catalog.md カタログのページと title の規則
packages/ui/src/tokens/ 3 つの層の CSS、書体の読み込み、生成したコードのテーマ
packages/ui/src/base/、src/case/、src/patterns/ コンポーネントと story とテスト。src/patterns/ は、パターンを置くときに作る
packages/ui/src/platform/ React に依存しない、ブラウザの判定と設定の関数。サブパスの export(@fukuemon/ui/ios-viewport)で配り、ページの読み込みの初めのスクリプトから呼ぶ
packages/ui/.storybook/ Storybook の設定と、カタログが返すレスポンスヘッダ(public/_headers)
packages/ui/wrangler.jsonc カタログを design.fukuemon.dev へ配信する Worker の設定
scripts/ トークンの CSS の生成、コントラストのチェック、色の値のチェック、ユーティリティのチェック、コードのテーマの生成

チェックは、パッケージ design-system の手順で入れ、CI で流す。

  • トークンの CSS とコードのテーマの生成物が、元のトークンと合っているか
  • セマンティックトークンの組のコントラスト比を、ライトとダークの条件ごとに
  • 画面のコードの色の値、プリミティブトークンの名前、原則に反するユーティリティ
  • コンポーネントの状態、押せる領域の大きさ、story のアクセシビリティ(ブラウザで動かすテスト)
  • Storybook のビルド