UI を書くときに毎回迷わないための決めごと。推測ではなく、いま動いているコードと、それを守っているテストの記述。
対象: shared/design-tokens/, app/assets/stylesheets/, app/rendering/, doc/assets/css/
関連: Working Memory(全体仕様)
色もサイズもフォントも、リテラルを CSS に直接書かない。すべて shared/design-tokens/ のトークンを参照する。
| ファイル | 中身 | |
|---|---|---|
| 色・影・角丸 | shared/design-tokens/theme.css |
--toddyi-* と --pico-* の上書き |
| 長さ | shared/design-tokens/sizes.css |
--toddyi-size-*(172 個) |
| ブレークポイント | shared/design-tokens/app-responsive.css / doc-responsive.css |
アプリ用・ドキュメント用で分離 |
shared/ にあるのは、Rails アプリと PHP のドキュメントサイトが同じ値を使うため。Dockerfile.doc が shared/design-tokens/*.css を doc イメージへコピーする。
これは好みの問題ではなく機械が見ている。tools/compliance/audit.rb の hardcoded_color / hardcoded_size / hardcoded_font が違反を CSV に出し、未解決が 1 件でもあると監査は落ちる。
Ruby / PHP からトークンを読むときは、それぞれの読み取り口を使う。
<%# ERB %>
<meta name="theme-color" content="<%= Design::Tokens.color('toddyi-primary') %>">
<img width="<%= Design::Tokens.pixel_attribute('toddyi-size-160px') %>">
<?php // PHP(doc/ 側) ?>
<meta name="theme-color" content="<?= DesignTokens::color('toddyi-primary') ?>">
<img width="<?= DesignTokens::pixelAttribute('toddyi-size-160px') ?>">
テラコッタ。--toddyi-primary を軸に、暗・明の 2 段を持つ。
| トークン | 値 | 用途 |
|---|---|---|
--toddyi-primary |
#b0764a |
主要アクション、theme-color、ブランドマーク |
--toddyi-primary-dark |
#8a5732 |
グラデーションの終点、hover |
--toddyi-primary-light |
#c9936b |
グラデーションの始点、控えめな強調 |
--toddyi-doc-bg |
#f3ece0 |
ドキュメント/PWA の background_color |
状態を表す色はアクセントとは別物。ブランド色で「危険」を表さない。
| トークン | 意味 |
|---|---|
--toddyi-error |
エラー。#d81b60 だったが --toddyi-primary との対比が足りず #c0392b に変更 |
--toddyi-danger |
破壊的操作(削除・アカウント削除) |
--toddyi-accent-green / --toddyi-accent-gold / --toddyi-accent-cyan |
図表・バッジの区別。意味は持たせない |
--toddyi-countdown-birthday / --toddyi-countdown-deadline |
Countdown のカテゴリ色 |
theme.css は :root に完全なライトパレットを置き、ダークは --pico-* を含めたトークンだけを上書きする。
罠: 色の定義が
@media (prefers-color-scheme: dark)や[data-theme]の中だけにあると、その状態でしか値が存在しない。必ず素の:rootに定義を置き、上書きは別ブロックで行う。
test/architecture/design_token_coverage_test.rbの「every Pico colour role our stylesheets use has a brand value」は、:rootブロックの中だけを見て検査する。ファイル全体を見ていた頃はこの区別を素通りしていた。
Pico CSS v2.1.1(ベンダリング済み)を土台にしているので、--pico-* を 92 個上書きしている。アプリ側の CSS が使う Pico のロールには、必ずブランド値が入っていることをテストが要求する。
app/rendering/typography.rb(PHP 側は doc/src/typography.php)が対応表を持つ。両方に同じ表があり、変えるときは両方。
| スクリプト | フォント | ロケール |
|---|---|---|
latin |
Inter | de, en, es, fr, id, it, pt, tr, vi, ru |
arabic |
Noto Sans Arabic | ar(RTL) |
devanagari |
Noto Sans Devanagari | hi |
japanese |
Noto Sans JP | ja |
korean |
Noto Sans KR | ko |
han |
Noto Sans SC | zh |
Inter がキリル文字も持っているので、15 ロケール中 10 が 1 つのダウンロードを共有する。ウェイトは wght@400;600;700 だけ。全ウェイトを取ると、描画しない字面のために転送量が数倍になる。
<html> が持つ属性<html lang="<%= I18n.locale %>"
dir="<%= Typography.direction_for(I18n.locale) %>"
data-script="<%= Typography.script_for(I18n.locale) %>">
data-script を CSS のフックにして typography_scripts.css が --toddyi-font-script を切り替える。ロケールごとに 1 つの <link> だけを出す(Typography.stylesheet_url_for)。
dir="rtl" で鏡像にするため、物理プロパティを書かない。
| 使う | 使わない |
|---|---|
margin-inline-start |
margin-left |
padding-block-end |
padding-bottom |
inset-inline-start |
left |
border-inline-start |
border-left |
text-align: start |
text-align: left |
test/architecture/logical_properties_test.rb が自前のスタイルシート全体を走査して落とす。
--toddyi-size-<値> の命名。16px → --toddyi-size-16px、0.85rem → --toddyi-size-0-85rem、100% → --toddyi-size-100pct。
sizes.css に追記してから使う。calc() の中でもトークンを使う: calc(var(--radius) + var(--toddyi-size-2px))。罠:
color-mix(in srgb, X 10%, transparent)の10%は長さではなく混合比。ここをサイズトークンに置き換えると、色の濃さがサイズ体系に紐づいてしまう。実際に--toddyi-size-10pctが「primary をどれだけ混ぜるか」を決めていた時期があり、見た目が同じなので誰も気づかなかった(#199 → #210)。監査はcolor-mix()を除外して検査する。
ボタンは Pico の要素スタイルを土台にする。<button> / [type=submit] / [role=button] に既定のスタイルが当たるので、多くの場合クラスは要らない。
| 書き方 | 見え方 | 使いどころ |
|---|---|---|
| クラスなし | 塗り(primary) | 画面の主アクション |
class="contrast" |
反転色の塗り | Upgrade / 保存など、さらに強い主張 |
class="outline" |
枠線のみ | 副次アクション |
class="outline secondary" |
弱い枠線 | 戻る・キャンセル(最頻出) |
class="icon-btn" |
正方形のアイコンボタン | サイドバー開閉、バーガー |
class="btn-text" |
文字だけ | インラインの弱いアクション |
class="outline auth-danger" |
危険色の枠線 | アカウント削除 |
読み込み順がそのままカスケード。components/buttons.css は application.css の後、機能別 CSS の前。ここを入れ替えると上書き関係が壊れる(app/views/layouts/application.html.erb のコメント参照)。
破壊的操作は必ず確認を挟む(data: { turbo_confirm: ... } または専用のモーダル)。
ar de en es fr hi id it ja ko pt ru tr vi zh。config/application.rb の available_locales が唯一の一覧で、test/features/i18n_coverage_test.rb がそれと突き合わせる。
| 層 | 出どころ |
|---|---|
| フレームワーク(Devise・バリデーション・日付) | rails-i18n / devise-i18n gem。zh だけは gem が zh-CN/zh-TW で持つため config/locales/zh/framework.yml が肩代わりする |
| アプリ自身の文字列 | config/locales/<locale>/{core,extensions,features}.yml |
| 公開サイト | doc/locales/runtime/languages/<lang>.php と doc/locales/pages/**/<lang>.json |
config.i18n.fallbacks = [:en])。つまり翻訳漏れは例外を出さず静かに英語になるので、目視ではなくテストで検出する。one/few/many/other、ar は zero/one/two/few/many/other。de や ja は素の文字列でよい。ru / ar は数によって名詞が変化するため、単一形だとほとんどの数で文法が壊れる。ActiveSupport::NumberHelper / I18n.l を使う。€9.98 は独語では 9,98 €、1200 は 1.200。この整形規則は rails-i18n が全ロケール分持っている。Billing::CheckoutLocale が明示的に対応付ける。Checkout にアラビア語とヒンディー語は無いので auto を渡してブラウザに委ねる。shared/brand/mark.svg が唯一の定義。角丸タイルにチェック、テラコッタのグラデーション。
コピー先は 3 つ(public/icon.svg, doc/assets/favicon.svg, doc/assets/img/mark.svg)で、バイト単位で同一であることをテストが要求する。ラスタ(public/icon.png 512px、public/favicon.ico 16/32/48/64)は同じ図形から起こす。
public/manifest.webmanifest は静的 JSON なのでトークンを読めない。代わりに theme_color / background_color が theme.css の値と一致することをテストが検査する。
shared/design-tokens/ に足すbin/rails test test/architecture — 色・トークン・論理プロパティ・マークの整合ruby tools/compliance/audit.rb — 未解決 0 件であることja と ar(RTL)で見る。片方だけ確認して壊した実績がある| 罠 | 症状 | 対処 |
|---|---|---|
| ダークだけで色を定義 | システム設定「自動」の閲覧者に、片方のテーマの文字がもう片方の背景に乗る | 素の :root に必ず定義を置く |
color-mix() の比率をトークン化 |
見た目は同じなので誰も気づかない | % が長さか比率かを見て判断する |
| 物理プロパティ | RTL で崩れる。ja/en では気づけない | ar で開く |
| 翻訳漏れ | 例外が出ず静かに英語になる | キー構造をテストで突き合わせる |
| マークのコピー忘れ | アプリだけ古いアイコンのまま(実際に赤い丸のままだった) | コピー同一性のテスト |
| 監査の偽陽性 | #110 のような Issue 参照が色に見える、トークン名の 160px が数値に見える |
ルール側を直す。CSS を歪めない |