Skip to content

スタイル・レイアウト・テーマ

4.1 方針

Kumiki は CSS を直接書かせない。CSS のカスケード・特異度・継承は AI にとって最大の隠れた依存源で、Kumiki の「副作用静的追跡」原則と相反する。

代わりに:

  1. デザイントークン をテーマで宣言
  2. 意味タグ にトークンを参照させる
  3. レイアウトはタイルプリミティブrow / column / grid)の props で表現
  4. どうしても必要なときだけ class / style props で素通し

これで普通の SPA に必要な見た目はカバーできる。再利用可能で任意のアニメーションは motion 定義(§4.9.1)が提供する。


4.2 デザイントークン

theme 定義で宣言する:

kumiki
theme DefaultTheme = {
    colors: {
        bg:        "#ffffff",
        fg:        "#1a1a1a",
        muted:     "#666666",
        primary:   "#0070f3",
        success:   "#0a7c2f",
        warning:   "#b07c00",
        danger:    "#c4222a",
        surface:   "#f7f7f7",
        border:    "#e0e0e0"
    },
    spacing: {
        xs: "4px",  sm: "8px",  md: "16px",
        lg: "24px", xl: "40px", xxl: "64px"
    },
    radius: {
        none: "0",   sm: "4px",   md: "8px",
        lg: "16px",  pill: "999px"
    },
    typography: {
        family: "system-ui, sans-serif",
        size: {
            xs: "12px", sm: "14px", md: "16px",
            lg: "20px", xl: "28px", xxl: "40px"
        },
        weight: {
            normal: "400", medium: "500", bold: "700"
        },
        line-height: "1.5"
    },
    shadow: {
        none: "none",
        sm:   "0 1px 2px rgba(0,0,0,0.1)",
        md:   "0 4px 8px rgba(0,0,0,0.1)",
        lg:   "0 8px 24px rgba(0,0,0,0.15)"
    },
    breakpoints: {
        sm: "640px", md: "768px", lg: "1024px", xl: "1280px"
    }
}

4.2.1 構文

theme-def ::= 'theme' identifier '=' '{' theme-section (',' theme-section)* '}'
theme-section ::= identifier ':' '{' theme-entry (',' theme-entry)* '}'
theme-entry ::= identifier ':' (string | '{' theme-entry (',' theme-entry)* '}')

theme は型 Theme の単一値。複数 theme を定義してダーク/ライトを切り替えられる。

4.2.2 app への適用

kumiki
app TodoApp
    caps   = []
    routes = {"/" -> Home, "/404" -> NotFound}
    init   = []
    theme  = DefaultTheme

4.3 トークン参照

tile prop の中でトークンを参照する場合、@ 接頭辞を使う:

kumiki
tile Card = box(
              column(
                heading("Title"),
                text("body"))) {
              style: {
                background: @colors.surface,
                padding:    @spacing.md,
                radius:     @radius.md,
                shadow:     @shadow.sm
              }
            }

@colors.surface は theme から解決される。テーマ切り替え時に自動で再描画される。

4.3.1 短縮プロパティ

頻出のスタイル props は 共通 props として提供され、@ を書かなくても解決される:

prop
bgcolor token namebg: "surface"@colors.surface
colorcolor token namecolor: "muted"
padspacing token namepad: "md"
pad-x, pad-yspacing token namepad-x: "lg"
gapspacing token namegap: "sm"
radiusradius token nameradius: "md"
shadowshadow token nameshadow: "sm"
sizetypography.size token namesize: "lg"
weighttypography.weight token nameweight: "bold"
kumiki
tile Card = box(
              column(
                heading("Title") {size: "lg", weight: "bold"},
                text("body") {color: "muted"})) {
              bg: "surface",
              pad: "md",
              radius: "md",
              shadow: "sm",
              gap: "sm"
            }

これにより、AI が書く UI のトークン消費が大幅に減る。


4.4 レイアウト

レイアウトは CSS ではなく タイルの構造で表現する。

4.4.1 row / column

kumiki
row(A, B, C) {gap: "md", align: "center", justify: "between"}
column(A, B, C) {gap: "sm", align: "stretch"}
prop
gapspacing token name
alignstart / center / end / stretch / baseline
justifystart / center / end / between / around / evenly
wraptrue / false

4.4.2 grid

kumiki
grid(A, B, C, D) {cols: 2, gap: "md"}
grid(A, B, C) {cols: [1, "auto", 1], gap: "sm"}     ; 数値 or 配列
prop
cols数値(等分) or List(Text)(CSS grid-template-columns 風)
rows同上
gapspacing token name
gap-x, gap-y個別指定

4.4.3 stack

stackvertical stackcolumn と意味的に同等のレイアウト(子を縦並びに積む)。視覚的な「積み重ね」のニュアンスがほしい時に使う。

kumiki
stack(Card1, Card2, Card3) {gap: "md"}

オーバーレイ(z 軸方向の重ね配置). z 軸方向に子を重ねるには overlay builtin を使う:

kumiki
overlay(Content, when(modalOpen, Modal())) {align: "center"}

overlay(...children)position: relative のコンテナをレンダリングする。最初の子がベース層(通常の文書フロー)、以降の子はオーバーレイとしてコンテナ上に絶対配置されるため、ベース層のレイアウトをずらさない。モーダル・トースト・ドロップダウン・ツールチップの土台となる。align prop が重ねる子を配置する:縦方向(top / bottom、既定は中央)と横方向(left / right、既定は中央)を - で連結する(例:top-leftbottomcenter〔既定〕)。認識できないトークンは center にフォールバックする。when(...) でオーバーレイの子を切り替えると、ベース層を乱さずに mount/unmount される。

4.4.4 panel / region / scroll / fieldset

builtin用途
panelグループ化ボックス。視覚的な境界 (border) や見出しを持つ
regiona11y 上の名前付き領域。スクリーンリーダー向け landmark
scrolloverflow auto なコンテナ。h 指定で固定高スクロール
fieldsetform 内のフィールドグループ。<fieldset> 相当
kumiki
panel(heading("Settings"), settingsForm) {bg: "surface", pad: "md"}
region(navList) {role: "navigation", aria-label: "Main"}
scroll(longList) {h: 400}

4.4.5 divider

水平線(<hr>)。区切り用:

kumiki
column(A, divider(), B)

4.4.6 box

汎用コンテナ。pad/bg/radius/shadow などで装飾する:

kumiki
box(content) {
    pad: "lg",
    bg: "primary",
    color: "bg",
    radius: "md"
}

4.4.7 サイズ

prop意味
wwidth。"full" / "auto" / "sm" / 数値(px)
hheight
min-w, min-h, max-w, max-hmin/max
aspect"1/1" / "16/9"
kumiki
image(src=url) {w: "full", max-w: 600, aspect: "16/9"}

4.5 レスポンシブ

スタイル props はオブジェクトでブレイクポイント分岐できる:

kumiki
column(A, B, C) {
    gap: {base: "sm", md: "md", lg: "lg"},
    pad: {base: "md", lg: "xl"}
}

grid(A, B, C, D) {
    cols: {base: 1, md: 2, lg: 4}
}

キーは base + theme.breakpoints のキー(sm, md, lg, xl)。


4.6 ダークモード

複数 theme を定義し、slot theme-name を切り替える:

kumiki
theme Light = {colors: {bg: "#fff", fg: "#000", ...}, ...}
theme Dark  = {colors: {bg: "#0a0a0a", fg: "#fff", ...}, ...}

slot themeName : Text = "Light"

reducer toggleTheme
    on=ui.click(ThemeBtn)
    do= themeName := if themeName == "Light" then "Dark" else "Light"

app App
    caps   = []
    routes = {"/" -> Home, "/404" -> NotFound}
    init   = []
    theme  = themeName        ; slot を直接指す

theme = themeName のように slot を指定すると、その値が変わるたびにテーマが切り替わる。themeName の値は宣言された theme 名のいずれか(コンパイラがチェック)。

4.6.1 OS 設定への追従

kumiki
reducer initTheme
    on=app.start
    do= themeName := if prefers-dark() then "Dark" else "Light"

prefers-dark() は組み込みヘルパ(prefers-color-scheme: dark を読む)。


4.7 状態スタイル(hover, focus, etc.)

タイルプリミティブは状態別 props を持つ:

kumiki
button(text="Save") {
    bg: "primary",
    color: "bg",
    hover: {bg: "primary-dark"},      ; トークン未定義なら警告
    focus: {shadow: "md"},
    disabled: {bg: "muted", color: "border"}
}

サポートされる状態キー:hover / focus / active / disabled / selected / checked


4.8 アイコン

icon 要素は名前で参照する:

kumiki
icon(name="check") {size: "md", color: "success"}

組み込みアイコンセットを v0.1 で 100 個程度提供する予定(リストは後日)。カスタムアイコンは theme.icons でパス登録:

kumiki
theme MyTheme = {
    ...,
    icons: {
        logo: "M3 3h18v18H3z..."     ; SVG path
    }
}

4.9 アニメーション (v0.1 では限定)

v0.1 では以下のみ:

prop効果
transition: "fade"フェードイン/アウト
transition: "slide-up"下からスライド
transition: "slide-down"上からスライド
transition-duration: "fast" / "normal" / "slow"速度

when で表示切替したタイルに自動適用される:

kumiki
when(modalOpen, Modal() {transition: "slide-up", transition-duration: "normal"})

4.9.1 motion 定義 (v0.2)

再利用可能で任意(ただし閉じた文法)のアニメーション — スピナー、パルス、独自の入退場 — には motion を宣言する。これは theme と同格のトップレベル定義(純粋に表示用の定義で、7 つのロジックレイヤーには含めないlanguage.ja.md §1.1.1 参照)であり、任意の tile の motion プロップから参照する。

kumiki
motion Spin = {
    keyframes: {from: {rotate: 0}, to: {rotate: 360}},
    duration:  "slow",        # "fast" | "normal" | "slow"、または正の Int(ミリ秒)
    easing:    "linear",      # linear | ease | ease-in | ease-out | ease-in-out
    iteration: "infinite",    # 正の Int、または "infinite"
    direction: "normal"       # normal | reverse | alternate | alternate-reverse
}

tile Loader = box(icon(name="spinner")) {motion: "Spin"}
  • keyframes(必須)は fromto のレコードを持ち、各々は閉じたアニメ可能プロパティ集合上のレコード(生 CSS 無し):

    プロパティ単位アニメ対象
    opacity0..1不透明度
    translate-x / translate-ypx(数値)位置
    scale数値大きさ
    rotatedeg(数値)回転

    1 ストップ上の複数 transform プロパティは、記述順によらず固定順translate-xtranslate-yscalerotate — で単一の transform に合成される(CSS transform は非可換なので、決定論のため順序を固定している)。未知プロパティはコンパイルエラー(E0401)、不正な keyframes(from/to 無し)は E0403

  • タイミングフィールドは任意(既定 duration:"normal"easing:"ease"iteration:1direction:"normal")。閉じた集合外の値は E0402

  • 未定義の motion を指す motion: "X" プロップは E0107

  • body はリテラルレコードなので、motion は slot の読み書きや effect emit ができない — 純粋に表示用。when(...)overlay と合成でき、生成 keyframes はスコープされる(グローバル CSS を漏らさない、§4.10)。prefers-reduced-motion: reduce で motion(と上記 v0.1 transition)を無効化する。

繰り延べ:多段パーセンテージ keyframes、色/blur/skew プロパティ。


4.10 グローバル CSS / リセット

ランタイムは最小リセット CSS を埋め込む。アプリ側からの追加は 意図的に不可能

理由:グローバル CSS は AI が追跡できない暗黙依存になる。すべての装飾はタイル props で完結させる。

例外:<head> への meta タグ・OG 画像などは app.meta で宣言:

kumiki
app TodoApp
    ...
    meta = {
        title: "My Todos",
        description: "Personal todo app",
        og-image: "/og.png",
        favicon: "/favicon.ico"
    }

4.11 設計上の判断記録

判断理由
CSS を直接書かせないカスケードと特異度が AI に追跡不能な暗黙依存を生む
デザイントークンを theme に集約スタイル値の散逸を構造で防ぐ
短縮 props (bg, pad 等) を提供トークン消費を削減
レイアウトはタイル構造で表現レイアウト用 CSS を AI が学ぶ必要をなくす
グローバル CSS 禁止「どこから来たスタイルか」を必ず親 tile に紐付ける
アニメーション v0.1 は限定多すぎる選択肢は AI の判断を不安定にする
motion は生 CSS でなく閉じた文法の定義アニメーションを静的に特定でき AI が編集しやすい。グローバル CSS 無しの不変条件を維持

4.12 次