デザインシステム(packages/ui)¶
Overview¶
packages/ui(@fukuemon/ui)は、fukuemon.dev の画面が使うトークンとコンポーネントを持つ。
記事の画面と、コードを動かす画面(エディタ、ファイルのツリー、端末、プレビュー、HTTP の流れ)を、同じトークンとコンポーネントで組む。
値は fukuemon.dev が決め、アクセシビリティ、タイポグラフィ、配色、レイアウトの指標と参考に、デジタル庁デザインシステム(DADS)を使う。
トークンの層とコンポーネントの分け方は、buddy の packages/ui と同じ形にする。
- 出典: デジタル庁デザインシステム
- ADR-0024: トークンを、fukuemon.dev の値のプリミティブトークン、セマンティックトークン、テーマ変数の 3 つの層にし、生成りの地と旧サイトの差し色で、角丸と影を使わない版面にする
- ADR-0021: packages/ui に Base と Case の React のコンポーネントを置き、packages/ui の Storybook をカタログにする
- ADR-0028: 操作できる要素と重なる面の角を 2px で丸め、重なる面にずらした影を付け、罫を引く動きを筆の線にする
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 を出さないだけでビルドが通るためである。 - プリミティブトークンは、セマンティックトークンとテーマ変数が参照する値だけを持つ。
- 地と罫と差し色は旧サイトの値で、書体の太さ、文字の大きさ、行高は DADS 2.1.0 の値である。例外は、12px と 13px の文字の大きさ(ADR-0062)と、
code-compactの行高の 154%(ADR-0067、提案)である。 - セマンティックトークンの
focusは、墨のプリミティブトークンを指す。
- 地と罫と差し色は旧サイトの値で、書体の太さ、文字の大きさ、行高は DADS 2.1.0 の値である。例外は、12px と 13px の文字の大きさ(ADR-0062)と、
- セマンティックトークンの種類は、面、文字、枠、操作、フォーカス、状態、区別、コード、挿絵の 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 の色相のとおり、成功を緑、エラーを赤に当て、注意を錆、情報を藍にする。どの値も、生成りの地の上で選ぶ。
- 出典: DADS の色
- ADR-0024: トークンを、fukuemon.dev の値のプリミティブトークン、セマンティックトークン、テーマ変数の 3 つの層にし、生成りの地と旧サイトの差し色で、角丸と影を使わない版面にする
文字と面の組は、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 になる。
- 14px より小さい文字を使わない。例外は、コードを動かす画面のコンポーネントの compact の 4 つ(12px と 13px)である(ADR-0062、ADR-0067)。
code-denseだけが、本文でない 14px の文字になる。
余白と区切りと重なり¶
- 余白は、Tailwind のデフォルトの段(1 段 4px)で書く。ブロックの間は 8px の倍数にする。DADS は余白の段の値を決めず、使う側に任せている。
- 出典: 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)にする。
- ペインの紙の面は 10px(
- コードを動かす画面の外では、角丸の名前を
rounded-controlの 1 つだけ使う。 - 丸めるのは、
Button、IconButton、boxedのTextField、TreeViewの行、Disclosureの要約、タブの閉じるボタン、Dialog、ActionMenu、検索の候補の一覧である。 Switchのトラックとつまみ、Checkboxの枠も丸める。ConsoleEntryListのログレベルのボタンも丸める。Tabsのタブ、下罫だけの検索の欄、メニューの項目、Notice、StatusBadge、Table、コードブロックの角は丸めない。罫で示す要素と、罫で区切る面のためである。
- コードを動かす画面だけ、角を 3 つの段で丸める。紙の面の重なりを、角の丸めで見分けるためである(ADR-0073)。
- 面の上に重なる面(ダイアログ、メニュー、候補の一覧)は、
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)。
- コードを動かす画面の配置だけ、高さが 500px 未満の画面を custom variant の
- 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 も、一覧の行のフォーカスを、行の内側の枠で示す。
- ADR-0066: コードを動かす画面の詰めたペインで、フォーカスの輪を要素の内側に引く
- ADR-0074: 詰めた木の行の中の名前の欄に内側の輪を引き、欄の行には輪を引かない
検索の欄(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 の箱に罫を描くと、罫が下線から離れて箱の下端に出るためである。
- 記事の一覧の題のように文中に置かないリンクを flex の子にして折り返したときは、罫は最後の行の下にだけ、リンクの幅で出る。flex の子の
- ナビゲーションは、折り返すかで線の形を分ける。
- サイトのヘッダーのナビは、今いるページの印を錆の筆の線にし、ページを開くと左から 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 のページの構成は、カタログの文書(
- 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 は、ブラウザのタブを閉じるキーと衝突するので割り当てない。
- 閉じるボタンはマウスで押すためのもので、読み上げと Tab のキーの順から外す。タブの並び(
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 の文字だけのボタンは持たない。文字だけの操作は
- コードの色を持つ。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 のビルド