CSS Utilities

CSS Utilities

Luna は MoonBit アプリケーション向けのゼロクライアントサイドランタイム CSS ユーティリティを提供します。

概要

Luna CSS は同一のクラス名を生成する2つのメカニズムを提供します:

  1. MoonBit ランタイム (@css): SSR 時にクラス名を生成

  2. 静的抽出 (luna css extract): ビルド時に .mbt ファイルから CSS を抽出

両方とも DJB2 ハッシュを使用して決定論的なクラス名を生成し、一貫性を保証します。

動作の仕組み

ビルド時                                ランタイム(ブラウザ)
──────────────────────────────          ──────────────────────────────
.mbt ファイル                           HTML: <div class="_z5et">
    │                                   CSS:  ._z5et{display:flex}
    ▼
luna css extract → CSS ファイル          CSS 生成コードなし!
    │
@css.css("display", "flex")
    ▼
"_z5et" を返す

基本的な使い方

MoonBit API

// 単一プロパティ
let flex_class = @css.css("display", "flex")  // "_z5et"

// 複数プロパティ
let card = @css.styles([
  ("display", "flex"),
  ("padding", "1rem"),
])  // "_z5et _8ktnx"

// 疑似クラス
let hover_bg = @css.hover("background", "#2563eb")

// メディアクエリ
let responsive = @css.at_md("padding", "2rem")

// 要素で使用
div(class=flex_class, [...])

Vite プラグイン(推奨)

Tailwind のように CSS をインポート:

// vite.config.ts
import { lunaCss } from "@luna_ui/luna/vite-plugin";

export default defineConfig({
  plugins: [
    lunaCss({
      src: ["src"],       // .mbt ファイルを含むソースディレクトリ
      verbose: true,
    }),
  ],
});
// main.ts
import "virtual:luna.css";  // 全ての抽出された CSS

CLI コマンド

# 全 CSS を抽出
luna css extract src -o dist/styles.css

# ディレクトリ単位で分割(コード分割用)
luna css extract src --split-dir --output-dir dist/css

# HTML に注入
luna css inject index.html --src src

Vite プラグインオプション

interface LunaCssPluginOptions {
  src?: string | string[];     // ソースディレクトリ
  split?: boolean;             // ディレクトリ単位の分割を有効化
  sharedThreshold?: number;    // 共通 CSS の最小使用回数(デフォルト: 3)
  verbose?: boolean;           // ログ出力を有効化
}

仮想モジュール

モジュール説明
virtual:luna.css全ての抽出された CSS
virtual:luna-shared.css共通 CSS のみ(split モード)
virtual:luna-chunk/{dir}.cssディレクトリ単位の CSS(split モード)

Split モード

コード分割を行う大規模アプリケーション向け:

lunaCss({
  src: ["src"],
  split: true,
  sharedThreshold: 3,  // 3回以上使用 → 共通 CSS
})
// 共通 + ページ固有の CSS をインポート
import "virtual:luna-shared.css";
import "virtual:luna-chunk/todomvc.css";

ベストプラクティス

1. 文字列リテラルを使用

静的抽出はリテラルでのみ動作します:

// ✓ 良い - 抽出可能
@css.css("display", "flex")

// ✗ 悪い - 抽出不可
let prop = "display"
@css.css(prop, "flex")

非リテラル引数を使用した場合:

  • 静的抽出では検出できない

  • MoonBit ランタイムはクラス名を生成する

  • しかし CSS ルールは抽出ファイルに含まれない

  • 結果:要素にクラスはあるが、対応する CSS がない

2. CSS は SSR コードで使用

ゼロランタイムオーバーヘッドのため:

// ✓ 良い - SSR コンポーネント(サーバーサイドのみ)
fn my_component() -> @static_dom.Node {
  div(class=@css.css("display", "flex"), [...])
}

// ✗ 避ける - Island コンポーネント(ブラウザで実行)
fn my_island() -> @luna.Node[Unit] {
  // @css モジュールがクライアントバンドルに含まれてしまう!
  div(class=@css.css("display", "flex"), [...])
}

// ✓ Island では事前計算されたクラス文字列を使用
fn my_island() -> @luna.Node[Unit] {
  div(class="_z5et", [...])  // @css インポート不要
}

理由: @static_dom.Node コンポーネントはサーバーでのみ実行されます。@luna.Node コンポーネントはブラウザで実行されるため、@css をインポートすると CSS 生成コードがクライアントバンドルに含まれます。

3. Island での動的スタイリング

// クラス名の切り替え
let class_name = if is_active.get() { "_active" } else { "_inactive" }
div(class=class_name, [...])

// CSS カスタムプロパティ
div(style="--color: " + color.get(), [...])

// 動的な値にはインラインスタイル
div(style="transform: translateX(" + x.get().to_string() + "px)", [...])

開発モード

欠落 CSS 検出

開発用ランタイムは欠落した CSS ルールを自動検出し、ランタイムで生成しつつコンソールに警告を出力します:

import {
  initCssRuntime,
  css,
  hover,
  hasClass,
  getGeneratedCount
} from "@luna_ui/luna/css/runtime";

// オプションで初期化
initCssRuntime({
  warnOnGenerate: true,  // 警告を表示(デフォルト: true)
  verbose: false,        // 全生成をログ出力
});

// CSS が事前抽出済みなら既存クラスを返す
// 欠落していればランタイムで生成して警告
const cls = css("display", "flex");
// Console: [luna-css] Generated at runtime: ._z5et { display: flex }
//          → Consider running 'luna css extract' to pre-generate CSS

// クラスがスタイルシートに存在するか確認
if (!hasClass("_z5et")) {
  console.log("CSS ルールが欠落しています!");
}

// ランタイム生成されたルール数を取得
console.log(`${getGeneratedCount()} 件のルールがランタイムで生成されました`);

ユースケース

  1. 開発時ホットリロード: MoonBit コード編集時に欠落 CSS を検出

  2. 本番問題のデバッグ: 静的抽出で捕捉できなかった CSS ルールを特定

  3. 動的 CSS フォールバック: 抽出漏れがあってもスタイルが動作することを保証

ランタイム API

関数説明
initCssRuntime(opts)オプションで初期化
css(prop, val)ベーススタイルを生成/確認
hover(prop, val)hover スタイルを生成/確認
hasClass(className)クラスがスタイルシートに存在するか
getGeneratedCss()ランタイム生成 CSS を文字列で取得
getGeneratedCount()ランタイム生成ルール数
resetRuntime()ランタイム状態をクリア(テスト用)

警告: 開発用ランタイムはバンドルに約 5KB 追加されます。本番ビルドではインポートしないでください。

API リファレンス

ベーススタイル

関数戻り値
css(prop, val)クラス名"_z5et"
styles(pairs)スペース区切り"_z5et _abc"
combine(classes)結合"_z5et _abc"

疑似クラス

関数セレクタ
on(pseudo, prop, val)カスタム
hover(prop, val):hover
focus(prop, val):focus
active(prop, val):active

メディアクエリ

関数条件
media(cond, prop, val)カスタム
at_sm(prop, val)min-width: 640px
at_md(prop, val)min-width: 768px
at_lg(prop, val)min-width: 1024px
at_xl(prop, val)min-width: 1280px
dark(prop, val)prefers-color-scheme: dark

生成(SSR のみ)

関数説明
generate_css()ベーススタイルのみ(css(), styles())
generate_full_css()全スタイル(ベース + 疑似 + メディア)
reset_all()全レジストリをクリア(テスト用)

出力例:

/* generate_css() */
._z5et{display:flex}

/* generate_full_css() */
._z5et{display:flex}
._1i41w:hover{border-color:#DB7676}
@media(min-width:768px){._abc{padding:2rem}}

関連項目