デザインレイヤー
Wippy のフロントエンドは、独立して公開された多数のモジュールが1つのアプリケーションへレンダリングされたものです。置き場所として自明なのは2つ、すべてのサーフェスが利用するテーマと、自分自身を所有するモジュールです。その2つの間にある領域は自明ではなく、そこに重複が蓄積します — 複数のモジュールが実際に共有しているのに、テーマに対応するコンポーネントがない概念です。
このページでは3つのレイヤーに名前を与え、それらを選び分けるためのテストを示し、それぞれの選択がうまくいった場合と失敗した場合の姿を示します。
レイヤー
| レイヤー | 届く範囲 | 所有するもの |
|---|---|---|
| テーマ | 自分が所有していないモジュールを含むすべてのサーフェス | PrimeVue コンポーネント、共有のセマンティックトークン、ドキュメント化されたクラス |
| 共有デザインレイヤー | オプトインしたモジュールのみ | それらのモジュールが共有する語彙のうち、テーマ化されたコンポーネントを持たないもの |
| モジュール | 自分自身 | 1つのサーフェスに固有と言えるもの |
テーマは普遍的であり、それが制約になる
テーマは自分が所有していないマークアップにスタイルを当てます。どのモジュールも — あなたのアプリを見たことのない誰かが書いたサードパーティ製プラグインを含めて — 同じホストへレンダリングされ、同じテーマによって描画されます。それがテーマを普遍的なレイヤーたらしめている理由であり、両方向に効いてきます。
アプリ固有のものをテーマに入れてはなりません。 それを求めていないすべてのモジュールに押し付けることになるからです。
モジュールは、アプリ固有のものがテーマにあることに依存してはなりません。 契約は PrimeVue コンポーネント + Wippy の共有セマンティックトークン + ドキュメント化されたクラス であり、アプリケーションが上乗せしたものは含みません。PrimeVue 自身のプリセットも契約ではないことに注意してください。Wippy は PrimeVue を theme: 'none' で動かすため、依拠するのは Wippy のセマンティックトークンです。
/* GOOD — Wippy の共有セマンティックトークン。すべてのモジュールに存在する */
.my-panel {
color: var(--p-text-color);
background: var(--p-content-background);
border: 1px solid var(--p-content-border-color);
}
/* BAD — アプリ固有のトークン。モジュールが1つのアプリの中でしか動かなくなり、
他の場所では宣言が静かに失われる。未定義のカスタムプロパティは計算値の時点で
宣言を無効にするため、宣言は破棄され、要素は黙って継承する。 */
.my-panel { background: var(--kx-surface-2); }
これは*「共有の語彙をファサードに置けるか?」*への答えでもあります。任意の、所有していないマークアップに本当に届く必要がある場合に限ります。自分のモジュール群に閉じているなら、それはテーマに属しません — 1つ下のレイヤーに属します。
バックボーンと、コンポーネントがオプトアウトしてよい場合
ホストが同梱する PrimeVue と Tailwind は、あらゆるコンポーネントで推奨されるバックボーンです。コンポーネントはオプトアウトできます — ただし、慣習的なものを何かレンダリングした瞬間にオプトアウトの余地は狭まり、はしごは一方向にしか進みません。
| コンポーネントが… | ロードすべきもの |
|---|---|
| プレゼンテーション中立である — canvas、SVG、コントロールもトークンもユーティリティもスクロールもないチャート | なし: hostCssKeys: [] |
| セマンティックトークンまたはダークモードを利用する | themeConfigUrl |
| スクロールできる | iframeCssUrl |
| markdown をレンダリングする | markdownCssUrl |
| Tailwind で表現できるものをレンダリングする | Tailwind — 手書きの CSS ではなくユーティリティを書く |
| PrimeVue がコンポーネントを提供しているものをレンダリングする — ボタン、入力、フォーム、テーブル、ダイアログ、メニュー、タグ、ツールチップ、あらゆるフィードバックコントロール | primeVueCssUrl かつ PrimeVuePlugin |
canvas 上のチャートは、正当なオプトアウトの典型例です。古典的な UI を持たないため、バックボーンのどれも必要としません。同じチャートにツールバーを付ければ、もはやプレゼンテーション中立ではありません — そのボタンは PrimeVue のボタンであり、統合一式が付いてきます。
結び付きに注意してください。Tailwind のユーティリティは primeVueCssUrl とともに配信されます。 独立した Tailwind 用のホスト CSS キーはないため、実際には Tailwind を必要とするコンポーネントは PrimeVue のアセットも読み込むことになります。(preflightCssUrl はキーの union には含まれません。shadow root の内側で Tailwind の preflight がどうしても必要なら、命令的にロードしてください — めったに必要ありません。)
このページにとっての実際的な帰結はこうです。モジュールが求めるものの大半は、すでにバックボーンに存在します。 共有デザインレイヤーはその上に載る狭い帯であって、PrimeVue と Tailwind がすでにカバーしているものをやり直す場所ではありません。仕組みについては CSS インジェクション を参照してください。
共有デザインレイヤー
既知のモジュール群にまたがって繰り返し現れ、テーマにコンポーネントが存在しない概念があります。コンテンツカード、サーフェスのヘッダー行、サーフェスが何も持たないときに表示するもの、タグのサイズ展開など。実在し、共有され、そして居場所がありません。
これらは公開パッケージとして配布され、ビルド時に各コンシューマーへマテリアライズされます。コンシューマーは別のリポジトリに存在するため、パスエイリアスではなくパッケージでなければなりません — このレイヤーの反証可能なテストは、別のリポジトリにあり、プロデューサーへのパスアクセスを持たないモジュールが、その語彙を利用してビルドできることです。
プロデューサー側のモジュールはそのパッケージをビルド時アーティファクトとして宣言し、各コンシューマーがそれを自身のツリーへマテリアライズします。宣言方法、node-package フォーマット、ランタイムが調整してくれる内容、そしてビルド側が自前で用意しなければならないつなぎについては ビルド時アーティファクト を参照してください。
モジュール
それ以外のすべてと、共有語彙からの意図的な逸脱すべてです。
何をどこに置くか決める
順に問い、最初の yes が勝ちます。
- それは値か? 色、角丸、余白、エレベーション、severity。 → テーマ。 セマンティックトークンを読みます。リテラルは決して使いません。
- テーマはすでにこれに対応するコンポーネントを提供しているか? Button、Dialog、Select、Tag。→ テーマ。 そのコンポーネントを使います。スタイルはコンポーネントにクラスを付けて当てます — 決して作り直しません。
- 2つ以上のモジュールがこの同じ概念を必要とし、その背後にテーマ化されたコンポーネントがないか? → 共有デザインレイヤー。
- それ以外 → モジュール。
引っかかりやすいのは質問2で、その背後には鋭いルールがあります。
実例
以下の例は Kickside — このレイヤーが生まれる前、モジュール CSS の 15.4% が完全な複製だった Wippy アプリケーション — から取ったものです。
テーマ化されたコンポーネントを作り直さない
PrimeVue は Button を提供しています。Kickside の9つのモジュールはそれをオプトアウトし、ネイティブの <button> に .kx-btn を手書きしていました。別の7モジュールはコンポーネントを使っていました。どちらの方言も局所的には妥当でした — ボタンを置く共有の場所がなかったため、アプリの半分がボタンを発明したのです。互いに突き合わせてみると、一致していたのは font-size と line-height だけでした。
Bad: .kx-btn .kx-btn-primary を付けたネイティブの button 要素 — テーマがすでに提供しているコンポーネントの2つ目の実装です。(ここで意図的にセレクターとして書いています。ドキュメントのゲートはサンプルコード中のネイティブなプロダクトコントロールを拒否します。これはこのルールを1つ上のレイヤーで強制したものです。)
Good: テーマ化されたコンポーネント。調整が必要ならクラスを付けます。
<Button label="Save" class="kx-save" />
テーマ化されたコンポーネントが合わないとき、それは作り直してよい許可ではありません。コンポーネントにクラスを付け、そのクラスにスタイルを当てます — 調整がアプリ全体なものならファサードで、局所的ならモジュールで。Kickside の knowledge モジュールは今もネイティブボタンに .kn-btn / .kn-primary を付けています。それは未完了の移行であって、真似すべきパターンではありません。
severity はテーマのものであって、あなたのものではない
severity — success、danger、warn、info — は公開されたランプを持つテーマのセマンティクスです。Kickside はこれを4つの命名体系にまたがって16回再導出していました(tone-gn、t-ok、kx-tone-success、tone-success)。同じクラス名が3つのモジュールで3つの異なる色を意味していたため、そのうちどれか1つの定義を公開すれば、他を黙って塗り替えていたはずです。
/* BAD — モジュールローカルな名前で severity を再導出している */
.tone-gn { color: #16a34a; }
/* GOOD — テーマから来た severity */
.status-dot.success { background: var(--p-success-500); }
トーンは共有レイヤーに存在してもかまいません — ただし装飾的なカテゴリー色としてのみであり、severity としてはいけません。「これは失敗した」を意味しうるなら、それは severity であり、テーマのものです。
テーマに居場所がない共有語彙
/* GOOD — PrimeVue は Card も surface Header も EmptyState も提供していない。
これらはモジュールをまたいで繰り返し現れ、背後にテーマ化されたものがない。
まさに共有レイヤーの対象。 */
@import "@kickside/ui-kit/kx-card.css";
@import "@kickside/ui-kit/kx-state.css";
採用するとは、import して削除すること
CSS の @import はシート内の他のすべてのルールに先行しなければなりません。したがって共有シートは常に最初に来るため、その後にモジュールが宣言したものは、同じ詳細度なら共有シートに勝ちます。パッケージを import しながら自前のコピーを残しているモジュールは、何ひとつ変えていません。
/* BAD — import は無効化されており、ローカルのコピーが勝ったまま */
@import "@kickside/ui-kit/kx-card.css";
.kx-card { border-radius: 14px; border: 1px solid var(--p-content-border-color); }
/* GOOD — import し、ローカルのコピーを削除し、ドキュメント化された差分だけを残す */
@import "@kickside/ui-kit/kx-card.css";
/* このサーフェスのカードは密なリスト内でインライン表示されるため、浮き上がりをなくす。 */
.kx-card:hover { transform: none; }
残すのは差分だけです — 本体全体を書き直してはいけません。そして2つの意図を1つの名前にまとめてはいけません。あるクラス名が2つのモジュールで別のものを意味するなら、それは1つの名前をまとった2つの概念です。名前を分けてください。勝者を選んで敗者を塗り替えるのではなく。
テーマに対する詳細度
モジュールの CSS は shadow root へ最初に注入され、テーマの PrimeVue シートはその後に追加されます。どちらも <style> 要素なので、ドキュメント順が決め手であり、テーマが後です。テーマ化されたコンポーネントのクラスに勝たなければならないモジュール側のルールには、より高い詳細度が必要です — ファイル内でより後ろの行ではありません。(adoptedStyleSheets が運ぶのはファサードのカスタム CSS であってテーマではないため、adopted なシートに頼ってもこれには勝てません。)
これが最も痛いのはパススルークラス、つまり自分のクラスがテーマ化された要素に付く場合です。
/* BAD — このクラスは PrimeVue 自身のフッター要素に適用されるため、
詳細度が同じならテーマが勝ち、padding は決して適用されない。 */
.kx-modal-foot { padding: 14px 18px; }
/* GOOD — ダイアログのルート配下にスコープし、テーマより高い詳細度にする */
.kx-modal > .kx-modal-foot { padding: 14px 18px; }
共有レイヤーに置いてよいもの
モジュール群が実際に共有していて、テーマが所有していないものすべてです。CSS の語彙、派生トークン、内部コンポーネント、ヘルパー、テストハーネス。重複の種類は同じです — Kickside には複製された CSS と並んで、1つのテストブートストラップのコピーが19個ありました。
セマンティックな単位で配布してください。 各ユニットは、コンシューマーが理解できる1つの名前付き概念であるべきです — kx-card、kx-state、kx-tag。コンシューマーが必要なものだけを取れるよう、より粒度の細かいパッケージを優先してください。明確に命名された複数のユニットを1つのパッケージで配布するのも成立はしますが、目指すべき形ではありません。
受け皿を作らないこと。 common も shared も misc も utils もいけません。中身が何かを名前が語らないユニットは、他に行き場のなかったものをすべて集め、このレイヤーが解決するはずだった問題を再構築することになります。
正規化は視覚的な変更である
ずれたコピーを統合すればピクセルが動きます。Kickside にはあるセレクターに対して17種類の本体にわたる19の定義がありました。すべての本体を diff し、正典を選び、なぜそれを選んだかを記録し、意図的な逸脱はドキュメント化されたオーバーライドとして残してください — そして結果を目で見てください。ユニットテストはレイアウトを見られません。
関連
- テーミング — トークンのカタログと、テーマがホストと子の両方へ届く仕組み
- コンプライアンスチェックリスト — フロントエンドが照合されるモジュール単位のルール
- ビルド時アーティファクト — パッケージの宣言と、コンシューマーへのマテリアライズ
- 依存関係管理 — モジュールが利用するものの宣言と解決