Skip to content

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

4.1 方針

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

代わりに:

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

これで普通の SPA に必要な見た目はカバーできる。複雑なアニメーションや凝った装飾は v0.2 で motion レイヤを追加予定。


4.2 デザイントークン

theme 定義で宣言する:

strand
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 構文

ebnf
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 への適用

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

4.3 トークン参照

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

strand
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"
strand
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

strand
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

strand
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 と意味的に同等のレイアウト(子を縦並びに積む)。視覚的な「積み重ね」のニュアンスがほしい時に使う。

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

z 軸方向の重ね配置(オーバーレイ)は boxposition を直接 prop で指定する方法、または将来追加予定の overlay builtin を使う。

4.4.4 panel / region / scroll / fieldset

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

4.4.5 divider

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

strand
column(A, divider(), B)

4.4.6 box

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

strand
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"
strand
image(src=url) {w: "full", max-w: 600, aspect: "16/9"}

4.5 レスポンシブ

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

strand
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 を切り替える:

strand
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 設定への追従

strand
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 を持つ:

strand
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 要素は名前で参照する:

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

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

strand
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 で表示切替したタイルに自動適用される:

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

任意の CSS transition / keyframe は v0.2 の motion レイヤで導入。


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

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

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

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

strand
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 の判断を不安定にする

4.12 次