パッケージの作成
共通の CSS ソースを公開し、各アプリで公開トークンを上書きして、本番用の利用アプリでパックしたパッケージを確認します。
このページの内容
#概要
CSS ソースを公開して、安定したスタイルの語彙を共有します。各利用アプリは自身の Master CSS エントリーにソースをインポートし、必要なトークン、ユーティリティ、ネイティブコンポーネントをコンパイルします。一つのアプリで語彙が動いてからここへ進んでください。その段階はスタイルの再利用で説明しています。
この形式のパッケージでは、アプリの生成スタイルシートやマニフェストではなくソース定義を公開します。アプリは自身のクラス利用、トークンの上書き、配信モードを選べます。ランタイムインテグレーションも提供するパッケージには、別の成果物の契約があります。
定義を共有し、アプリごとにコンパイルする
両方のアプリが同じ CSS ソースをインポートします。ネイティブコンポーネントは既定で含まれ、各アプリは自身のマークアップが要求するユーティリティを生成します。
テキストによる説明
- ソースを共有する. パッケージはネイティブな .btn コンポーネントと必要時生成の content-auto ユーティリティを定義します。
- アプリ A. マークアップは btn を使います。ネイティブコンポーネントは含まれ、未使用の content-auto は出力されません。
- アプリ B. マークアップは btn と content-auto を使います。出力にネイティブコンポーネントと要求したユーティリティが含まれます。
#パッケージを作成する
例では @acme/ui-theme を使います。公開前に自分のスコープへ置き換えてください。利用側はパッケージ名をインポートし、エクスポートする CSS ファイルは master.css です。
- my-master-theme
- package.json
- master.css
- README.md
パッケージルートを master.css に向けます:
{ "name": "@acme/ui-theme", "version": "0.1.0", "files": ["master.css"], "style": "./master.css", "exports": { ".": { "style": "./master.css", "default": "./master.css" } }, "sideEffects": ["./master.css"]}この形式にはバンドラー、TypeScript ビルド、マニフェスト生成、コンパイル済み CSS 出力は不要です。パッケージはソース CSS を公開し、Master CSS 対応プロジェクトが自身のスタイルシートグラフの一部としてコンパイルできます。
#master.css を書く
利用プロジェクトに供給する共通の語彙を master.css に置きます。
@theme { --color-brand: #4f46e5; --color-text-action: var(--color-text-strong); --color-focus-action: var(--color-text-strong); --spacing-action-x: 1rem; --radius-action: 0.375rem;}@custom-media --theme-motion (prefers-reduced-motion: no-preference);@utility content-auto { content-visibility: auto; contain-intrinsic-size: auto 32rem;}@layer components { .btn { display: inline-flex; align-items: center; justify-content: center; gap: var(--spacing-xs); min-height: 2.375rem; max-width: 100%; padding: .5rem var(--spacing-action-x); border: 1px solid var(--color-line-divider); border-radius: var(--radius-action); font-size: var(--font-size-sm); font-weight: 400; line-height: 1.2; background-color: var(--color-surface-raised); color: var(--color-text-action); box-shadow: var(--shadow-sm); &:hover { box-shadow: var(--shadow-md); } &:active { background-color: var(--color-surface-inset); box-shadow: none; } &:focus-visible { outline: 2px solid var(--color-focus-action); outline-offset: 3px; } }}例はカラーマーカー、ネイティブな .btn コンポーネント、必要時に生成する content-auto ユーティリティ、名前付きの theme-motion 条件を供給します。共通状態はコンポーネントが持ち、トランジションの利用はアプリが選びます。ネイティブなコンポーネント CSS は既定で含まれ、ユーティリティは使用時だけルールを生成します。
利用側に必要な最小限の公開語彙を選びます:
- 色、間隔、半径、タイポグラフィ、影、モーション、コンテナーのトークンなどの共通値には
@themeを使います。 - メディア名には
@custom-media、ほかの再利用ラッパーには@contentsを持つ@mixinを使います。 - 必要時に生成するクラスには
@utility、ネイティブ CSS 内の再利用には@mixinを使います。 btn、field、card、toolbarなどの意味を持つ UI 語彙を、パッケージが意図的に担当する場合だけネイティブの@layer componentsを使います。
トークンの命名はテーマトークン、構文は CSS ディレクティブ、抽象の選択はグローバルスタイルを参照してください。
#パッケージを使う
インストール後、既定の Master CSS エントリーの後にインポートします:
@import '@master/css';@import '@acme/ui-theme';インポートしたソースは、プロジェクトの CSS エントリーグラフの一部になります。マークアップで共通の語彙をユーティリティやネイティブ CSS クラスとして使えます:
共通パッケージの語彙
<article class="p-lg b-solid b(1) b-line-divider r-lg content-auto"> <p class="flex items-center gap-xs m(0) text-xs fg-text-muted"><span aria-hidden="true" class="h(8) w(8) pill bg-brand"></span>Package brand</p> <h2 class="mb-xs mt-sm text-lg font-regular">Shared vocabulary</h2> <p class="mt(0) mb-md text-sm fg-text-muted">The app supplies the markup; the package supplies the shared action.</p> <button type="button" class="btn (transition-property:box-shadow,background-color)@theme-motion (transition-duration:var(--duration-fast))@theme-motion (transition-timing-function:var(--easing-smooth))@theme-motion">Preview action</button></article>生成された CSS
@layer components { .btn { justify-content: center; align-items: center; gap: var(--spacing-xs); max-width: 100%; min-height: 2.375rem; padding: .5rem var(--spacing-action-x); border: 1px solid var(--color-line-divider); border-radius: var(--radius-action); font-size: var(--font-size-sm); background-color: var(--color-surface-raised); color: var(--color-text-action); box-shadow: var(--shadow-sm); font-weight: 400; line-height: 1.2; display: inline-flex; &:hover { box-shadow: var(--shadow-md); } &:active { background-color: var(--color-surface-inset); box-shadow: none; } &:focus-visible { outline: 2px solid var(--color-focus-action); outline-offset: 3px; } }}@layer theme { :root, :host { --color-zinc-5: oklch(96.5% .0011 265); --color-zinc-40: oklch(68.6% .0028 265); --color-zinc-60: oklch(48.6% .003 265); --color-zinc-90: oklch(24.6% .0022 265); --color-zinc-100: oklch(15% .0016 265) } :root, :host { --color-surface-inset: light-dark(var(--color-zinc-5), var(--color-zinc-100)); --color-surface-raised: light-dark(var(--color-white), var(--color-zinc-90)); --color-line-divider: light-dark(oklch(0% 0 none / .12), oklch(100% 0 none / .12)); --color-text-strong: light-dark(var(--color-zinc-100), var(--color-white)); --color-text-muted: light-dark(var(--color-zinc-60), var(--color-zinc-40)) } :root, :host { --color-white: oklch(100% 0 none) } :root, :host { --font-weight-regular: 400; --font-size-xs: .75rem; --font-size-sm: .875rem; --font-size-lg: 1.125rem; --text-xs: var(--font-size-xs); --text-xs--line-height: 1.5; --text-xs--letter-spacing: 0; --text-sm: var(--font-size-sm); --text-sm--line-height: 1.5; --text-sm--letter-spacing: 0; --text-lg: var(--font-size-lg); --text-lg--line-height: 1.5; --text-lg--letter-spacing: 0; --radius-lg: .5rem; --spacing-xs: .5rem; --spacing-sm: .75rem; --spacing-md: 1rem; --spacing-lg: 1.5rem; --duration-fast: .15s; --easing-smooth: cubic-bezier(.4, 0, .2, 1) } :root, :host { --shadow-sm: 0 0 0 1px light-dark(oklch(0% 0 none / .04), oklch(100% 0 none / .05)), 0 1px 1px -.5px light-dark(oklch(0% 0 none / .045), oklch(0% 0 none / .3)), 0 2px 2px -1px light-dark(oklch(0% 0 none / .04), oklch(0% 0 none / .26)), 0 4px 4px -2px light-dark(oklch(0% 0 none / .04), oklch(0% 0 none / .24)); --shadow-md: 0 0 0 1px light-dark(oklch(0% 0 none / .05), oklch(100% 0 none / .06)), 0 1px 1px -.5px light-dark(oklch(0% 0 none / .045), oklch(0% 0 none / .3)), 0 2px 2px -1px light-dark(oklch(0% 0 none / .04), oklch(0% 0 none / .26)), 0 4px 4px -2px light-dark(oklch(0% 0 none / .04), oklch(0% 0 none / .24)), 0 8px 8px -4px light-dark(oklch(0% 0 none / .045), oklch(0% 0 none / .26)) } :root, :host { --color-brand: #4f46e5; --color-text-action: var(--color-text-strong); --color-focus-action: var(--color-text-strong); --spacing-action-x: 1rem; --radius-action: .375rem }}@layer utilities { .text-lg { font-size: var(--text-lg); line-height: var(--text-lg--line-height, normal); letter-spacing: var(--text-lg--letter-spacing, normal) } .text-sm { font-size: var(--text-sm); line-height: var(--text-sm--line-height, normal); letter-spacing: var(--text-sm--letter-spacing, normal) } .text-xs { font-size: var(--text-xs); line-height: var(--text-xs--line-height, normal); letter-spacing: var(--text-xs--letter-spacing, normal) } .content-auto { content-visibility: auto; contain-intrinsic-size: auto 32rem } .items-center { align-items: center } .bg-brand { background-color: var(--color-brand) } .b-line-divider { border-color: var(--color-line-divider) } .r-lg { border-radius: var(--radius-lg) } .pill { border-radius: calc(infinity * 1px) } .b-solid { border-style: solid } .b\(1\) { border-width: calc(1 * 1px) } .fg-text-muted { color: var(--color-text-muted) } .flex { display: flex } .font-regular { font-weight: var(--font-weight-regular) } .gap-xs { gap: var(--spacing-xs) } .h\(8\) { height: calc(8 * 1px) } .m\(0\) { margin: calc(0 * 1px) } .mb-md { margin-bottom: var(--spacing-md) } .mb-xs { margin-bottom: var(--spacing-xs) } .mt-sm { margin-top: var(--spacing-sm) } .mt\(0\) { margin-top: calc(0 * 1px) } .p-lg { padding: var(--spacing-lg) } .w\(8\) { width: calc(8 * 1px) } @media (prefers-reduced-motion: no-preference) { .\(transition-duration\:var\(--duration-fast\)\)\@theme-motion { transition-duration: var(--duration-fast) } } @media (prefers-reduced-motion: no-preference) { .\(transition-property\:box-shadow\,background-color\)\@theme-motion { transition-property: box-shadow, background-color } } @media (prefers-reduced-motion: no-preference) { .\(transition-timing-function\:var\(--easing-smooth\)\)\@theme-motion { transition-timing-function: var(--easing-smooth) } }}プロジェクトのエントリーがインポートしたパッケージは、すでにローカルスタイルシートから使えます。一つのスタイルシートのコンパイル時コンテキストだけに加えるには、@reference を使います:
@reference '@acme/ui-theme';.button { padding-inline: var(--spacing-action-x); border-radius: var(--radius-action); background-color: var(--color-brand);}ローカルの .button はパッケージの間隔、半径、ブランドのトークンを使います。@reference はこのスタイルシートのコンパイル中に定義を供給し、出力には必要なテーマリソースが含まれます。ネイティブな .btn ルールや生成ユーティリティクラスはコピーしません。それらが必要ならパッケージの CSS を別に読み込んでください。ローカルスタイルシートの使い方はルートレベルのスタイルを参照します。
#利用側で公開値を上書きする
アプリ固有の選択はパッケージのインポート後に置きます。たとえばこのアプリは、コンポーネント CSS をコピーせず共通のアクションを広げます:
@import '@master/css';@import '@acme/ui-theme';@theme { --spacing-action-x: 1.5rem;}.btn は引き続き var(--spacing-action-x) を読みます。アプリは横パディングを 1rem から 1.5rem に変え、ほかのアプリは各自の値を保ちます。利用側が依存できるトークン名、コンポーネントセレクター、ユーティリティ名を文書化してください。JavaScript がなくても公開名の変更は、利用側のマークアップやスタイルシートに影響します。
値を上書きし、コンポーネントを保つ
利用側の上書きはパッケージのインポート後に置きます。ガイドはルート 16px で 1rem と 1.5rem を比べ、.btn のルールは共通のままです。
テキストによる説明
パッケージとアプリが一つの .btn 定義を共有する。アプリは --spacing-action-x を変え、ボタンの役割を保ったまま横パディングを広げる。
#アプリの方針をアプリに置く
再利用する記述用パッケージが、一つのアプリのソース走査、フィルター、配信方法を決めるべきではありません。
以下は利用アプリに置きます:
- アプリ固有の
@sourceと@source notのルール。 - アプリ固有の
@safelistと@blocklistの方針。 - ルート、ページ、コンポーネントの一回限りのクラス。
- 生成マニフェスト JSON とコンパイルした生成 CSS 出力。
パッケージには共通の語彙を、アプリの CSS エントリーにはローカルなソース境界、配信の選択、アプリ固有の方針を置きます。
#例で検証する
パッケージディレクトリーからソースをパックします。npm pack はローカルのアーカイブを作り、レジストリーには公開しません:
npm pack例の名前とバージョンでは、アーカイブは acme-ui-theme-0.1.0.tgz です。master.css と想定する package.json のエクスポートが入っているか確認してください。files リストは無関係なソースやビルド成果物を除き、npm は標準のメタデータファイルも含めます。
Master CSS を統合済みの小さな利用アプリへ、そのアーカイブをインストールします:
npm install ../my-master-theme/acme-ui-theme-0.1.0.tgznpm run build上記のマークアップを走査対象のソースに置きます。本番出力でネイティブな .btn、要求した content-auto、必要なテーマリソースを確認してください。アプリを開いてボタンの計算済みパディングを調べ、ローカルな上書きでも繰り返します。通常状態に加え、ホバー、キーボードフォーカス、動きを抑える設定も確認します。
パッケージ解決の検証には、フレームワークインテグレーションのビルドを使います。現在の単独 CLI の検査は、アプリのエントリーを読み込んでも、サードパーティのベアパッケージインポートをスタイルシートグラフ外に残す場合があります。依存一覧にパッケージの master.css がなければ、その語彙は未検証です。本番の利用アプリは、ローカルのソースだけのプレビューでは調べられないファイルとエクスポートパスも検証します。



