zul-writer について
zul-writerは、ZK開発者向けにZULページの作成、検証、プレビューと、対応するJavaコントローラーの生成を行うスキルです。必要な工程だけを実行でき、ページ作成前にUI画像を分析できます。
主な機能
- 01必要なページ作成工程のみ実行
- 02ZULファイルの生成と検証
- 03ViewModelまたはComposerの作成
- 04ページのレンダリングと確認
- 05スクリーンショットからレイアウト分析
活用シーン
- 01要件やモックアップからZULページを作成
- 02既存ページの解析可否を確認
- 03ページをプレビューして表示不具合を診断
- 04ページに対応するコントローラーを生成
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ zkoss-demo/ agent-skill --skill zul-writerインストール後、Claude Code で/zul-writerを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
assets/borderlayout-example.zulcontent-tabbox.zuldata-grid-selection-mvvm.zuldialog-popup-mvvm.zulexample-data-management-mvvm.zulexample-simple-form-mvvm.zulexample-simple-list-mvc.zulflexible-sizing.zulform-validation-mvvm.zulkanban-board.zulKanbanViewModel.javamaster-detail-mvvm.zulmvc-sample.zulmvvm-pattern-structure.zulMyComposer.javaMyViewModel.javatemplate.zulzul.xsdreferences/charts-guidelines.mdcontroller-guidelines.mdpreview-guidelines.mdui-to-component-mapping.mduse-case-guidelines.mdscripts/_skill_meta.pydetect-pattern.pypreview-zul.pyvalidate-zul.py詳細
ZUL ライター
ワークフローの概要
このスキルでは、5 つのステップで構成されるプロセスを通じて、適切に構成された zul ページを作成します。
- 要件の確認 - ページの目的、パターン、レイアウトの要件を収集します
- ZUL の生成 - 要件に基づいて ZUL ファイルを作成します
- ZUL の検証 - 生成された ZUL が正しいことを確認します
- コントローラークラスの生成 - 対応する Java クラス(ViewModel または Composer)を作成します
- プレビューとセルフレビュー - ページを画像にレンダリングし、要件に照らして確認します
別の開始方法:ユーザーが UI 画像(スクリーンショットやモックアップ)を提示した場合は、まず以下のビジュアル分析を行ってから、5 つのステップのプロセスに進みます。
リクエストに必要なステップだけを実行する
5 つのステップはそれぞれ個別の入口であり、一連の手順ではありません。何もないところからページを作る場合はすべて必要ですが、それ以外のほとんどのリクエストでは 1 つか 2 つで十分です。完成済みの .zul を渡して見た目を尋ねる人には、Step 5 だけで十分です。MVC と MVVM のどちらかを聞き出したり、ついでにマークアップを「改善」したりすると、相手が尋ねていない質問に答えることになり、満足していたページを台なしにしてしまいます。
| ユーザーの依頼 | 実行するステップ |
|---|---|
| 「…するページを作って」「このモックアップを ZUL にして」 | 1 → 5、ワークフロー全体 |
「foo.zul のプレビュー/スクリーンショット/見た目を見せて」 | 5 |
| 「このアイコンが空のボックスになるのはなぜ?/このラベルが切れるのはなぜ?/このセクションが表示されないのはなぜ?」 | 5 の後、要素を --probe |
| 「この ZUL は有効?」「このページを解析できないのはなぜ?」 | 3 |
「ZK 10 に <togglebutton> はある?」「<charts> に sclass を指定できる?」 | どちらでもありません。スキーマから --describe で回答します(Step 2、ガイドライン 3) |
| 「このページの ViewModel を書いて」 | 4 |
| 「このページのデータを ViewModel/Composer に移して」 | Step 2 の抽出パス → 4 → --run-controllers を付けて 5 |
| 「このグリッドに列を追加して」「サイドバーを狭くして」 | 既存のファイルに対して 2 → 3 → 変更が見た目に表れる場合は 5 |
| 「すでにあるこの ViewModel/Composer 用のページを作って」 | 1 → 5。ただし、抽出するものはありません。Step 2 の モデル駆動型ページ を参照してください |
モデル駆動型ページでは Step 5 を 2 回実行します。2 回目は修正のためではありません。 番号付きステップは、データがまだマークアップ内にあるページの実行順です。Step 5 でレイアウトが確定したら、Step 2 の抽出パスでデータをコントローラーに移し、--run-controllers を付けてもう一度レンダリングし、確認します。つまり、1 → 5 の後に、2 → 4 → 5 を一度実行します。番号を逆にたどるため、最後のループはうっかり忘れやすいものです。
「見た目がおかしいのはなぜ?」は診断の依頼であり、検証の依頼ではありません。 この 2 つの項目が並んでいるのは、混同しやすいためです。「このページを解析できないのはなぜ?」と「このアイコンが空のボックスになるのはなぜ?」は似ていますが、Step 3 で答えられるのは前者だけです。<label sclass="z-icon-bell"/> は有効な ZUL です。バリデーターは問題なく通し、ページに問題がないと報告します。それこそが、実際の不具合が「問題なし」として処理されてしまう状況です。バリデーションに合格しても正しくレンダリングされないページは、Step 3 では見つけられません。マークアップはすでに書かれています。問題はブラウザーがそれをどう表示したかであり、答えられるのはレンダリングだけです。Step 5 に進んで要素をプローブし、マークアップに手を加える前に計測結果から原因を特定してください。推測から始めると、やり直しが発生し、間違った行を自信満々に「修正」してしまうこともあります。「なぜ」という質問への答えは、原因を特定することそのものです。編集は別の依頼であり、それを求めるかどうかはユーザーが決めます。
省略したステップの情報も、実行するステップで必要になります。Step 4 ではページが MVC か MVVM かを把握する必要があり、Step 5 ではレンダリングを評価する基準が必要です。Step 1 からやり直すのではなく、ファイルとユーザーのメッセージから読み取ってください。既存の .zul には、そのパターン、レイアウト、ZK のバージョンが記述されており、聞き取りで得られる情報よりもはるかに信頼できます。本当に推測できないことだけを尋ねてください。
1 つのステップだけを依頼された場合は、書き換えずに報告してください。 自分が書いていないページの検証やレンダリングを行うと、頼まれていない変更点が見つかることがあります。見つかったことを伝え、依頼された編集だけを行ってください。それ以外の修正は、頼まれるまで待ってください。ただし、そのステップ自体を妨げる不具合は例外です。解析できないページはレンダリングできないため、問題を伝えて修正を提案してください。
ビジュアル分析(画像/モックアップ向け)
UI のスクリーンショットまたはモックアップ画像が提供された場合は、5 ステップのワークフローを開始する前に、次の分析を行います。
- ビジュアルの分解:すべての UI 要素(レイアウト、入力欄、ボタン、テーブル、ナビゲーション)を特定します。
- コンポーネントとレイアウトの方針:ZK コンポーネントへのマッピング(references/ui-to-component-mapping.md を参照)を計画し、全体的なレイアウト(例:
<borderlayout>、入れ子になった<vlayout>)を決定します。 - タブのコンテンツ範囲:タブがある場合は、コンテンツの境界を決定します。タブの切り替えに応じて切り替わる項目は、
<tabpanel>の中に配置する必要があります。assets/content-tabbox.zul を参照してください。 - カスタムスタイルの特定:フォールバックの HTML 要素またはカスタム CSS が必要な領域を示します。
移行:これらの分析結果を、ステップ 1:ユーザー要件の確認に活用し、最終的にはステップ 2:ZUL ファイルの生成に反映します。
ステップ 1:ユーザー要件の確認
ニーズを把握するため、的を絞った質問をします。画像から始める場合は、ビジュアル分析の結果を質問に反映します。
未解決の点だけを尋ねます。依頼内容、プロジェクト、既存のファイルから、すでに複数の答えがわかる場合があります。たとえば、pom.xml に記載されたバージョン、参照するよう指示されたマークアップに見えるパターン、プロンプトに記載されたレイアウトなどです。こうした点を再度尋ねると、確認していないかのような印象を与えます。
質問できる相手がいない場合もあります。スクリプトによる実行、バッチジョブ、応答が途絶えたユーザーなどです。それを理由に作業を止めてはいけませんが、答えを勝手に作り上げてもいけません。質問の後にある尋ねる相手がいない場合を参照してください。
尋ねる質問
1. ZK のバージョン
ユーザーのプロジェクトから検出します(pom.xml、ivy.xml、または build.gradle で ZK の依存関係のバージョンを確認してください)。見つからない場合は、次のどちらかを尋ねます。
- 9 以前
- 10.x
2. ページの目的
- データ入力フォーム
- データリスト/グリッド表示
- 複数のセクションがあるダッシュボード
- ダイアログ/ポップアップウィンドウ
- マスター詳細ビュー
- 検索と結果のページ
- その他:[具体的に指定]
3. MVC または MVVM パターン
どちらの選択肢も同等に提示し、どちらにも「(推奨)」と付けないでください。
- MVVM:
@bind/@commandデータバインディングを使用する ViewModel ベースのパターン。テスト可能ですが、ZK への理解がより必要です。 - MVC:
applyとコンポーネントのワイヤリングを使用する Composer ベースのパターン。シンプルで、初心者にも扱いやすい方法です。
両者の境界は明確なので、分類できないページはありません。MVC では Composer を適用し(apply="com.foo.MyComposer")、MVVM では ViewModel を設定し、バインディング構文を使用します
(viewModel="@id('vm') @init('com.foo.MyVM')" と @load/@bind/@command)。どちらもコントローラーであり、設定されたコンポーネントおよびそのすべての子孫を制御します。
**1 ページにつき 1 つのパターンを記述します。**ZK では両者を混在させることができ、実際に混在させているプロジェクトもあります。しかし、混在したページでは「この値はどこから来るのか」という問いに対する答えが 1 つに定まりません。この問いは、抽出処理とステップ 5 のセルフレビューの両方で重要です。ユーザーが両方を求めた場合は、そのページのデータがどちらのパターンに従うべきかを尋ねてください。
この質問に答えが得られない場合は、以下の尋ねる相手がいない場合を参照してください。既存のプロジェクトページの判断が既定値に優先される質問は、これだけです。
4. 静的データかモデル駆動か
これについては、独立した質問として尋ねてください。MVC/MVVM とは別の選択です。前者は振る舞いをどこに置くかを決め、こちらはデータをどこに置くかを決めます。また、作業の順序も変わります。
- 静的データ:テキストや行を ZUL に記述します。たとえば、
<label value="Acme Corp"/>のように記述し、グリッドでは<row>、リストボックスでは<listitem>、ツリーでは<treeitem>として行を指定します。レイアウト、モックアップ、デモなど、コンテンツが固定されているものに適しています。 - モデル駆動:コントローラーがデータを提供します。たとえば、Composer が配線したコンポーネントに対して
setModel()を呼び出す方法や、ページが@load/@bindとバインドされたmodelを介して読み取る ViewModel を使う方法があります。実データを扱うものに適しています。
コンポーネントが行を取得する方法は、どちらか一方のみです。両方は使えません。 モデルを設定すると、マークアップに記述した行は暗黙のうちに破棄されるため、モデルと並べて残したリテラル行は、何も表示されないマークアップになります。
どちらを選んでも、ステップ 4 ではコントローラーを作成します。モデル駆動のページは、さらに 2 回に分けて作成します。ステップ 2 のモデル駆動のページを参照してください。
5. レイアウト要件
- Borderlayout(north/south/east/west/center)
- 垂直レイアウト(vlayout)
- 水平レイアウト(hlayout)
- グリッドベースのレイアウト
- タブ付きレイアウト(tabbox)
- 複合レイアウト
6. ZK Charts(チャートが必要な場合のみ)
ZUL ページで <charts> コンポーネントが必要な場合は、チャートコードを生成する前に references/charts-guidelines.md に従ってください。
7. テーマとデータ密度
データを高密度で表示するページを設計する場合は、パディング、マージン、フォントサイズが小さいコンパクトな無料テーマ iceblue_c を使うようユーザーに提案してください。
質問できる相手がいない場合
上記の各質問には、未回答の場合に使える答えがあります。そのため、質問が未回答でも作業全体が止まることはなく、明示された選択肢を使えます。
| 質問 | 未回答の場合に使用 | 理由 |
|---|---|---|
| 1. ZK のバージョン | 10.x | 新たに決めるものではありません。validate-zul.py はすでに 10、preview-zul.py は 10.2.1 をデフォルトにしています。ここで別のデフォルトを設定すると、このスキルが自身のバリデーターに反する内容を書くことになります。 |
| 2. ページの目的 | デフォルトなし — リクエストから読み取る | ユーザーは常に、作成したいページについて説明しています。推測する必要はなく、固定のデフォルトを設定すると、すでに伝えられた内容を上書きしてしまいます。 |
| 3. MVC か MVVM か | MVC。ただし、プロジェクト内で意見が一致している場合を除きます — 以下を参照 | 誰も修正できない場合、Composer と @Wire のほうが正しく実装しやすいためです。 |
| 4. 静的データかモデル駆動か | 静的(リテラル行) | いずれにせよ、ページはここから始まります。モデル駆動のページのパス 1 は、リテラル行です。リクエストで実際のデータソースが指定された場合にのみ、モデル駆動を選んでください。 |
| 5. レイアウト | デフォルトなし — 目的から導き出し、その導出方法を説明する | レイアウトは、どのようなリクエストやモックアップでも、最も具体的な要素です。ここで固定のデフォルトを使うと、与えられた情報を無視することになります。 |
| 6. ZK Charts | 該当なし — ページにチャートが必要な場合のみ尋ねる | — |
| 7. テーマと密度 | プロジェクトのテーマを維持し、レポートで iceblue_c が利用できることを伝える | テーマを切り替えると、アプリ内のすべてのページが再描画されます。誰も見ていない状況で行うべきことではありません。 |
使用したデフォルトはすべて、最終レポートに明記してください。 デフォルトを設ける価値があるのは、予測可能で、後から確認できるためです。誰にも伝えられないデフォルトには、そのどちらの利点もありません。
プロジェクトですでに採用されているパターン(質問 3 のみ)
既存のページがすべて MVVM のプロジェクトであることは、推測ではなく根拠です。そこに MVC のページを 1 つ追加すると、デフォルト設定では検出できない形でコードベースが悪化します。そこで、質問 3 についてのみ、デフォルトに頼る前に調べます。
uv run <skill-base-dir>/scripts/detect-pattern.py <project-root>
ZUL 側だけを調べます。両方のパターンが属性で示されるのは、この側だからです。Java 側を正直に数えることはできません。ViewModel は通常のクラスであり、@Init は任意なので、これを数えると @Init を使わない ViewModel を見落とします。また、Composer のサブクラスを数えると、ページに適用されていないすべての基底クラスやヘルパーまで含まれてしまいます。
PATTERN: mixed (MVC 6, MVVM 2)
USE: mvc -- the project uses both, so it has no single answer to give
プロジェクトのパターンに従うのは、全員一致の場合のみです。つまり、PATTERN: mvc または PATTERN: mvvm の場合です。6 対 2 のような偏りがある場合も含め、それ以外の場合は MVC のデフォルトを維持します。すでにパターンが混在しているプロジェクトからは、どちらを望んでいるのか判断できないためです。いずれの場合も、スキャンで判明した内容と、どの回答を使用したかをレポートに記載してください。
残りの 6 つの質問は、このようには扱いません。パターンはコードベース全体にわたる単一のアーキテクチャ上の決定ですが、ページの目的とレイアウトはページごとに異なり、ZK のバージョンはアンケートではなくビルドファイルから取得できます。
ステップ 2:ZUL ファイルを生成する
生成ガイドライン
ZUL ファイルを生成する際は、次の技術ガイドラインに従ってください。
-
UI 要素をマッピングする:references/ui-to-component-mapping.md を参照して、適切な ZK コンポーネントを選択してください。
- ネイティブ HTML よりも ZK コンポーネントを優先してください。
<borderlayout>、<vlayout>、<hlayout>などのレイアウトコンポーネントを効果的に使用してください。
-
style属性ではなくクラスでスタイルを指定する:- ページの CSS はファイルの上部付近にある 1 つの
<style>要素に記述してください。<?style ?>処理命令には記述しないでください。ZK コンポーネントにはsclass(ネイティブのn:要素にはclass)を使って適用します。各クラスには要素の 種類 を表す名前を付け (sclass="stat-card")、ZK のz-クラスや他のページの CSS と衝突しないよう、ページ内の クラスには独自の短いプレフィックスを付けてください。 - 単なる好みの問題ではありません。
style属性はウィジェット自身の要素に出力されるため、 どのスタイルシートのルールよりも優先されます。その結果、ページのテーマを変更できなくなり、 後から記述した CSS は効かず、:hover、:focus、@mediaのルールも適用できません。 複数のコンポーネントに宣言を貼り付けると、似た値がそれぞれ別々に増えていきますが、 クラスなら一度編集するだけで、すべてのインスタンスに反映されます。 - クラスが「効かない」ように見える場合は、テーマ側の指定のほうが詳細です。
.z-buttonは.my-btnより詳細度が高いため、テーマ側が優先されます。styleに頼るのではなく、セレクターを (.my-page button.my-btn { … }) のように詳細にしてください。styleに頼ることこそ、ページの いたるところにインラインスタイルが使われる原因です。 - サイズと余白は CSS ではなくコンポーネントに指定します。
hflex/vflex、width、height、spacing、valignはコンポーネント独自の API であり、ZK のレイアウト エンジンと連携します。これらを CSS で再現すると、フレックスレイアウトが壊れます。 ボックスはコンポーネントに、見た目はクラスに任せてください。 - 例外は 1 つだけです。実行時にしか存在しない値です。 データに由来する色や幅
(
style="@load('background-color:'.concat(tag.color))")は、静的なクラスにはできません。 その宣言だけはインラインに残し、それ以外はクラスに記述してください。 - 代替のネイティブ HTML 要素(例:
<n:div>)を使う場合は、必要な CSS を同じ<style>ブロックに含めてください。
- ページの CSS はファイルの上部付近にある 1 つの
-
記述する前にスキーマを確認する — 同梱済みで、正確で、無料です:
これまで使ったことのないコンポーネントを記述する前、またはこれまで使ったことのない 属性をコンポーネントに指定する前に、次を確認してください。
uv run <skill-base-dir>/scripts/validate-zul.py --describe <component> [--attr <name>] --zk-version <detected-version># Does <charts> take sclass? -> no: className or zclass uv run ~/.claude/skills/zul-writer/scripts/validate-zul.py --describe charts --attr sclass # Does <togglebutton> exist in ZK 10? -> no such component uv run ~/.claude/skills/zul-writer/scripts/validate-zul.py --describe togglebuttonうろ覚えの 2 つの表記で迷ったときに、推測するのではなく、必ずこうしてください。 よくある 問題は、グリッドの作り方を知らないことではなく、もっともらしい表記を 3 つ知っていて、 そのうち誤ったものを選んでしまうことです。検証で後から問題を見つけるには、余計な作業が ひととおり必要になります。同じスキーマを先に確認すれば、その場ですぐに答えがわかります。 出力されたリストにない属性は、どれほどもっともらしく見えても受け付けられません。
留意すべき制限が 1 つあります。同梱されているスキーマは 10.x のドキュメントです。ZK 9 を 対象とする場合、スキーマにないことは参考にはなりますが、決定的な根拠ではありません。 該当する場合はその旨が示されます。
-
ZK ドキュメント:
- 利用できる場合は、
zk-docMCP サーバー(ツール:search_zk_docs)に問い合わせて、コンポーネントの詳細情報を確認してください。 - プロパティやイベントの詳細は ZK Javadoc を参照してください。
- 利用できる場合は、
-
ベストプラクティス:
- レスポンシブなレイアウトでは、固定ピクセル幅よりも
hflex/vflexを優先してください。hflex="min"はコンテンツに合わせてコンポーネントのサイズを調整します。hflex="1"のフィールドの横にある<button>などに便利です(assets/flexible-sizing.zul を参照)。 - 意味のある ID を使い、assets/template.zul の構造に従ってください。
- XML コメント内に
--を絶対に入れないでください。 XML ではの間のどこにも使用できないため、 Java のスタイルとしてはまったく問題のない区切り文字でも、 ZUL では解析エラーになります。代わりに=を使ってください:``。
- レスポンシブなレイアウトでは、固定ピクセル幅よりも
モデル駆動のページ:データを書き込んでから取り出す
値がコントローラーから取得されるページは、そのコントローラーが実行されるまで、ページ自体を表示できません。それまでは、@load(vm.customer) は薄く表示された式のテキストとして描画され、バインドされた model はプレースホルダーの行が数行表示されるだけです。そのため、列幅、折り返し、カードの高さ、統計の行が収まるかどうかは、ページに表示される実際のテキストではなく、それとは異なるテキストを基準に判断することになります。これが、Step 5 を通過したページが、実際に初めて実行すると期待どおりにならない理由です。
どちらの方法を取るべきかは、質問ではなく確認で判断できます。 ユーザーに尋ねる必要はありません。答えは目の前にあります。一から作成するページには、まだ作成していないコントローラーが必要です。ZK では、Composer や ViewModel は共有サービスレイヤーではなくページに属するため、新しいページには新しいコントローラーが必要で、以下の 2 パス方式を取ります。クラス名をすでに指定できるのは既存の .zul だけなので、その場合はファイルを開いてください。存在してコンパイルできるなら、抽出するものはありません。最初から --run-controllers で描画し、実際のデータを基準に判断してください。
データに該当するもの。 データが変化したときに値も変わるなら、それはデータです。データが何であっても変わらない単語は UI の固定要素であり、ZUL に恒久的に記述します。列見出し、ボタンのキャプション、セクションのタイトル、単位、空状態のメッセージ、検証ヒントなどが該当します。セルの値、リスト項目、件数、金額、ステータスラベルはデータです。判断基準は「テキストかどうか」ではありません。明日の行に対しても、この単語は正しいままだろうか、ということです。
Pass 1 — リテラルデータで実際の形を再現する
最初のバージョンは、置き換えるデータに似た形のリテラル値を使って作成します。現実的な長さの名前、実際の桁数の価格、表示領域を埋めるのに十分な行数を用意します。行はマークアップとして記述します。グリッドなら <row>、リストボックスなら <listitem>、ツリーなら <treeitem> を使います。リテラル値はそのまま描画されるため、Step 5 のスクリーンショットにはユーザーが実際に見るページが表示されます。また、このパスはバインドを使う方法よりもすっきりしています。@load がどこにもなければ、ページにプレースホルダーは表示されません。
このパスのために Step 4 で作成するコントローラーには、データではなく動作を記述します。イベントハンドラー、@Command、配線などです。今ここにデータを入れると、このパスの目的が失われます。Step 5 では --run-controllers を通して実際の行が描画され、確認していないテキストを基準にレイアウトを判断することになります。
レイアウトとスタイルが適切になるまで、ここで繰り返し調整します。
チャートには Pass 1 がありません。 <charts> にはリテラル形式がありません。<row> のようにマークアップ内で系列を記述できないため、2 パス方式は適用できません。最初からチャートのデータをコントローラーに記述し、最初の描画からその領域を --run-controllers で描画してください。その間、ページの残りの部分には引き続きリテラル値を使います。この場合、ページの最初の描画はコントローラーのコンパイルに依存します。そのため、表示されないチャートについて判断する前に CONTROLLERS: の行を確認してください。古い .class ファイルは、チャートが一度も描画されなかった場合とまったく同じように見えます。
Pass 2 — Step 5 でレイアウトが確定した後の抽出
ワークフローの中で Step 5 の後に実行される唯一の作業であり、前に実行するものではないため、見落としやすい項目です。実行の条件は、Step 5 の結果として「レイアウトが確定した」ことです。次の 4 つの作業を順番に行います。
-
各リテラルをコントローラーにフィールド、ゲッター、またはリストとして移動します。
-
ZUL から参照します。 MVVM では値を、その値を読み取るバインディングに置き換えます (
@load(vm.customer.name)、model="@load(vm.items)")。MVC には ZUL 側の式がありません。 Composer がコンポーネントのidを使って接続し、setModel(...)を呼び出すため、ZUL ではidをそのまま残し、それ以外は何も追加しません。例外は
<template name="model">で、2 つのパターンでは読み取り方が異なります。 テンプレートは アイテムごとにマークアップを繰り返すため、ページのパターンにかかわらず式が必要です。ページ テンプレート内 テンプレート変数 MVVM @load(node.data.name)バインダーは var="node"を尊重しますMVC ${each.data.name}— 通常の EL常に eachです。カスタムvarは黙って無視されますこれを逆に理解すると、1 ラウンド分の手戻りが発生します。解決できない変数はエラーではなく 空のテキストとして表示されるためです。MVC ツリーで
${node.data.name}を使ったところ、構造、 インデント、開閉状態、選択状態は正しいのに、すべてのラベルが空のツリーが生成されました。${each.name}を使って調査して初めて、実際のメッセージであるProperty 'name' not found on type DefaultTreeNodeが表示されました。このメッセージから、 ルールのもう一方もわかります。grid または listbox のテンプレートではeachはアイテム自体 (${each.action})ですが、tree ではTreeNodeであり、データはさらに 1 段深い場所にあります (${each.data.action})。 -
リテラルの行を削除します。 残しておいても問題ないわけではありません。モデルを設定すると、 バインディング経由でも
setModel()経由でも、内容があっても空でも、マークアップに記述された行は 破棄されます。しかも警告なしに黙って破棄されるため、マークアップには何も表示されない行が 残ったまま、ページは正しくレンダリングされます。画像を見ても誰も気づけないため、Step 5 のLAYOUTブロックにあるliteral-rows-discardedが該当件数を測定します。 -
それ以外は何も変更しません — コンポーネント、
sclass、hflexはそのままにします。抽出で移動するのは値であり、 構造ではありません。したがって、その後にページの表示位置がずれた場合は、抽出を確認してください。
ここでは、想定されるスキーマ上の指摘が 2 つあります。 どちらもページに問題があるのではなく、 付属の XSD による誤検出です — Layer 2 は ZK よりも厳格です。
- 子要素が
<template>だけの<tree>は失敗します。ツリーに子要素が 1 つでもあると、treeTypeはtreecolsまたはtreechildrenを要求します。空の<treechildren/>を追加すると 通ります。モデルによるツリーの生成は引き続き行われ、空要素はリテラルの行ではありません。 - リテラルの
<listitem>があるのに<listhead>がない<listbox>も同じように失敗します。 通常は<listhead>を追加するのが適切です。
<columns> とテンプレートを持つ <grid> は、記述どおりで問題ありません。この種の残りのケースについては、
test/known-failures.txt の B1 グループを参照してください。
次に --run-controllers を指定してもう一度レンダリングし、問題が解消したことを確認します。このレンダリングで
確認するのは抽出であり、レイアウトではないため、修正ラウンドではありません — 見つかった問題はすべて
Pass 2 の予算内で修正し、Pass 1 の残りの予算は使いません。Step 5 の How many rounds を参照してください。
レイアウトとコンポーネントのパターン
XML とパターン構造
- ベーステンプレート:assets/template.zul
- MVC 構造:assets/mvc-sample.zul
- MVVM 構造:assets/mvvm-pattern-structure.zul
サイズ設定とレイアウト
- 柔軟なサイズ設定(hflex/vflex):assets/flexible-sizing.zul
- Borderlayout の例:assets/borderlayout-example.zul
一般的な MVVM パターン
ステップ 3:生成した ZUL を検証する
このスキルのベースディレクトリ(スキルのコンテキストヘッダーに「Base directory for this skill:」として記載)にあるスクリプトを使って検証します。ステップ 1 で検出した ZK バージョンを --zk-version で渡し、レイヤー 4 のチェックが対象のバージョンに合うようにしてください。
uv run <skill-base-dir>/scripts/validate-zul.py --zk-version <detected-version> <path-to-zul-file>
例:スキルのベースディレクトリが ~/.claude/skills/zul-writer で、プロジェクトの対象が ZK 10.3.0 の場合は、次のように実行します。
uv run ~/.claude/skills/zul-writer/scripts/validate-zul.py --zk-version 10.3.0 path/to/file.zul
- レイヤー 1:XML の整形式チェック(依存関係なし)。複数ルートのフラグメントは、検証前に自動的に
<zk>でラップされます。 - レイヤー 2:XSD スキーマ検証(
lxmlが必要) - レイヤー 3:属性の配置チェック(
lxmlが必要)—誤った場所に配置された属性(例:textboxのiconSclass)を検出します。 - レイヤー 5:インラインスタイルに関する注意喚起 —
<style>に記述し、sclassで割り当てるべき静的なstyle="..."属性を列挙します。報告はされますが、実行は失敗しないため、各行を無視してよいノイズではなく、修正すべき問題として扱ってください。データ駆動のstyle="@load(...)"は意図的にスキップされます。 - レイヤー 4:対象の ZK バージョンに対する互換性チェック — すべての対象バージョンについて、削除済み/非推奨の API を確認します。また、ZK-10 専用 API(例:削除された
<fragment>や、新しいaccept/responsive属性)は--zk-versionを指定した場合にチェックされます。省略時の既定値は10です。 - レイヤー 6:実行時のセマンティクス — すべての静的チェックには適合していても、ページの構築中に例外を発生させるマークアップを検出します。現在対象となるのは、リテラル値の
selectedIndexです。<combobox selectedIndex="0"/>はレイヤー 1〜5 を通過しますが、Out of bound: 0 while size=0で失敗します。インデックスは、コンポーネント自身の子が追加される前、コントローラーが実行される前、バインダーがモデルを設定する前に適用されます。そのため、combobox、listbox、radiogroup、tabboxでは、マークアップの記述内容にかかわらず例外が発生します。項目を直接記述しても解決せず、model="..."も同様です。モデル駆動型の<listbox model="@load(vm.items)" selectedIndex="0">も同じように例外を発生させます。代わりに選択状態を指定してください。readonly の combobox ではvalue="..."、<listitem>/<radio>/<tab>ではselected="true"を使うか、モデルが設定された後にコントローラーから呼び出してください。selectedIndex="-1"は、一見したとおり万能な回避策ではありません。 ZK 9.6.6 と ZK 10.3.0.1 で同じ結果を確認しています。<tabbox selectedIndex="-1">は、まだ存在しない<tabs>子要素をセッターが必要とするため、依然としてNo tab at allで失敗します。tabbox ではリテラル値は安全ではありません。<cardlayout selectedIndex="-1">はOut of bound: -1 while size=2で失敗します。カードは先に追加されるため値の範囲がチェックされ、「何も選択されていない」状態はありません。0..cards-1の範囲内のカードインデックスなら問題ありません。selectboxはモデルの有無にかかわらず、どのリテラル値も許容するため、このレイヤーでは一切チェックしません。 - レイヤー 7:コントローラーとの照合 —
--controllerを渡した場合にのみ実行されます(ステップ 4 を参照)。
前提条件
レイヤー 2 と 3 には lxml が必要です。uv run は、スクリプトの PEP 723 インラインメタデータを使ってこれを自動的に処理します。一時的な環境に lxml をインストールするため、手動でのセットアップは不要です。uv を利用できない場合は、通常のインタープリターで実行してください。スクリプトがフォールバックとして lxml を自動インストールします。
python3 <skill-base-dir>/scripts/validate-zul.py --zk-version <detected-version> <path-to-zul-file>
(Windows では、python3 の代わりに python を使用してください。)
使用状況の追跡
このスクリプトを実行すると、匿名の集計済み使用状況データ(スキル名とバージョンのみ。識別情報なし)もバックグラウンドスレッドで送信されます。検証が遅延したりブロックされたりすることはありません。オプトアウトするには DO_NOT_TRACK=1 または TRACK_URL="" を設定するか、実行ごとに --dev を指定してください。スキルの開発中またはテスト中に実行する場合は使用状況にカウントされないよう、--dev を指定してください。
検証のみを依頼された場合
失敗したレイヤーを報告し、スクリプトが示した行を引用して、それぞれの意味を説明したら、そこで止めてください。検証の依頼は、何が問題なのかを伝えるよう求めるものです。ユーザーが修正を求めた場合、または修正がすでに取り組んでいる作業の一部である場合に、ファイルを編集してください。
検証後のチェックリスト
パターンの一貫性
- MVC:
apply属性を使用し、MVVM のバインディング式は使用しない - MVVM:
viewModel属性を使用し、適切なバインディング構文を使う - コンポーネントごとではなく、ページ全体で 1 つのパターンを使用する
modelとリテラルの行を両方持つコンポーネントを作らない — Step 2 の モデル駆動型ページ を参照
ベストプラクティス
- ID は各 ID 空間の所有者(
<window>、<idspace>)内で一意にする - 外観は
sclassで適用する<style>クラスに記述する。残すstyle属性は、値がデータに由来するものだけにする。見つかったその他の属性はレイヤー 5 に一覧表示される - 固定寸法よりも
hflex/vflexを優先する - アクセシビリティのため、意味のあるラベルとツールチップを含める
Step 4: コントローラークラスを生成する
ZUL ページに対応する Java コントローラークラス(ViewModel または Composer)を生成します。
静的データのページでも生成してください。 値が固定でも、ページには何らかの動作が必要です。開発者が求めているのは、関連付けの形です。Composer がコンポーネントを結び付けてそのイベントを監視する方法や、ViewModel が @Command を宣言し、それを呼び出すために ZUL に何を書くかです。これを省くと、ZK 固有で推測が難しい唯一の部分を提示できません。ページに実際にある要素(Save ボタン、行の選択、検索ボックスなど)を使い、マークアップ内の値に作用する、少なくとも 1 つの動作するハンドラーを含めてください。
今は動作、データは後で。 ページが静的かモデル駆動型かにかかわらず、ここで記述するのはページの動作です。モデル駆動型ページでも、この時点ではデータはマークアップ内にあり、レイアウトが確定するまではそこに置きます。そのため、setModel() も、バインドされた model を支えるゲッターも、まだ記述しません。これらは抽出パスで追加し(Step 2 の モデル駆動型ページ を参照)、このステップをもう一度実行して取り込みます。ここでデータも記述してしまうと、リテラルの状態を気づかないうちに失うことになります。Step 5 では実際の行が描画され、レイアウトが確認済みの内容に照らして評価されなくなります。
チャートの場合を除きます。 <charts> にはリテラル形式がないため、ほかのページ要素はすべてリテラルのままにして、最初の描画時からモデルをここに含めます。Step 2 の チャートには Pass 1 がない を参照してください。上記の段落に従ってチャートのデータを取り除くと、エラーを報告しない空のチャートが残ります。
コントローラー生成ガイドライン
-
パターンの一貫性:
- MVVM パターンでは ViewModel を使用します。
- MVC パターンでは Composer を使用します。
-
実装の詳細:references/controller-guidelines.md の技術要件に従ってください。
-
完了前に、ZUL と配線を照合してください。
--controllerを指定してバリデーターを再実行し、Layer 7 を有効にします。uv run <skill-base-dir>/scripts/validate-zul.py <path-to-zul> --controller <path-to-controller>このバリデーターは、このワークフローのほかの方法では検出できない不具合を 2 つ報告します。他のレイヤーでも、Step 5 のレンダリングでも検出できません。
@Wireフィールドの型が、指定された ID のコンポーネントと異なる場合。この状態でもコンパイルは成功し、すべての検証レイヤーを通過して、正しくレンダリングされますが、フィールドを初めて使用したときにClassCastExceptionが発生します。以前の実行では、<a>に対して@Wire Labelを指定してしまい、自分の Composer を読み直して初めて気づきました。@Wireフィールドで指定した ID が、ZUL 内のどのコンポーネントにも宣言されていない場合。フィールドは null のままになり、初回使用時にNullPointerExceptionが発生します。
Layer 7 は、確実に判断できない場合は何も報告しません。具体的には、あるクラスが別のクラスを継承するコンポーネントファミリー(
Textbox/Combobox、Checkbox/Radio、Box/Hbox、Row/Group、Listitem/Listgroup、Button/Combobutton)、Componentのような基底クラスのフィールド型、コレクション、#id以外のセレクター、<include>を含むページが該当します。Layer 7 が何も報告しないからといって、配線が正しいとは限りません。これは、証明できる範囲で問題が見つからなかったことを意味します。
MVC パターン - Composer クラス
MVVM パターン - ViewModel クラス
完全な例とパターン
Kanban ボードやダッシュボードのような複雑な UI パターンや、テンプレートの完全な例については、references/use-case-guidelines.md を参照してください。
Step 5: プレビューと自己レビュー
完成したページを画像にレンダリングし、実際に確認して、Step 1 で収集した要件と照らし合わせます。Step 2~4 で確認できるのはマークアップだけです。ページの実際の見た目を確認できるのはこのステップだけです。
プレビューのみを依頼された場合、照合する Step 1 の回答はありません。そのため、レンダリング結果をページ自体のマークアップとユーザーが期待すると述べた内容に照らし合わせて確認し、見た目を説明して、画像のパスを報告します。以下の 修正が必要な点 のリストは、報告すべき不具合の判断基準を示すものです。変更ではなく確認を依頼されたファイルを編集してよいという意味ではありません。
Step 3 と同じ方法で、このスキルのベースディレクトリからプレビュースクリプトを実行します。
uv run <skill-base-dir>/scripts/preview-zul.py <path-to-zul-file>
例:スキルのベースディレクトリが ~/.claude/skills/zul-writer/ で、ページが Maven の Web アプリにある場合は、次のように実行します。
uv run ~/.claude/skills/zul-writer/scripts/preview-zul.py src/main/webapp/index.zul
画像の出力先は現在の作業ディレクトリです。 --out を指定しない場合、PNG は作業中のディレクトリに、ページ名を基にしたファイル名で書き込まれます。たとえば index.zul の場合は ./index-preview.png となり、SCREENSHOT: の行に正確なパスが表示されます。そのままそこに置いてください。この画像は一時ファイルではありません。ワークフロー全体で生成される唯一の視覚的な成果物であり、ユーザーが開いて、ページが依頼どおりか確認するためのものです。ユーザーがクリックして確認、保存、削除できるよう、作業ファイルのそばに置く必要があります。代わりに一時ディレクトリやスクラッチパッドディレクトリに保存すると、ユーザーに伝えなければ見つけられず、後から再発見することも難しいパスに隠れてしまいます。再レンダリングでは同じファイルが上書きされるため、修正の各ラウンドを通じてパスは変わらず、最後に報告するパスとして引き続き使えます。--out を指定するのは、ユーザーが保存先を指定した場合、またはコーパスや CI ジョブで画像を特定の場所に集める必要がある場合に限ります。
このセッションでページのコントローラーを書き込んだ場合は、--run-controllers を追加します(実行するとプロジェクトコードが動くため、先に次の段落を読んでください)。
uv run ~/.claude/skills/zul-writer/scripts/preview-zul.py --run-controllers src/main/webapp/index.zul
--run-controllers を渡すタイミング。 このセッションでページのコントローラー(Step 4 の composer または ViewModel)を書いた場合に渡してください。コントローラー内のサンプルデータは自分で用意したものであり、実行することでスケルトンのスクリーンショットが評価可能なものになります。また、このフラグを指定すると、バインドされた値、モデルにバインドされた行、composer が設定したラベルが実際に表示されます。これは 抽出 の確認にも使うレンダリングです。ページ内のリテラルをコントローラーに移動した後(Step 2)、承認済みの状態とページの見た目が変わっていないことを確認できます。コントローラーを書いていないページには、絶対に指定しないでください。このフラグはプロジェクトのクラスパスから任意のプロジェクトコード(コンストラクター、サービス呼び出し、doAfterCompose の処理など)を実行するため、レンダリングごとに明示的に指定する必要があり、デフォルトにはなりません。コントローラーがまだコンパイルされていない場合は、先にビルドしてください(mvn compile / gradle classes)。クラスパスにコンパイル済みクラスがない場合、スクリプトは警告を表示します。正当な理由でページの処理に時間がかかり、劣化が続く場合に限り、--controller-timeout <seconds> を追加してください(レンダリング全体のデフォルトの制限時間は 10 s です)。
ビューポート:--width、--height、--full-page。 デフォルトのビューポートは 1280x900 です。出力の SIZE: 行には、レンダリングで実際に使われたビューポートが常に表示されます。
- モックアップの幅に合わせます。 ユーザーからスクリーンショットまたはモックアップが提供された場合は、その画像のおおよそのピクセル幅を
--widthに指定し、同じ条件で画像を比較できるようにしてください。たとえば、幅が 1600 px のモックアップには--width 1600を指定します。幅の広いモックアップに対してデフォルトの 1280 でレンダリングすると、ページではなく自分が原因の差異が生じ、その修正に時間を費やすことになります。ファイルのピクセル数ではなく、レイアウト の幅に合わせてください。高 DPI で書き出された画像なら半分にし、狭いビューポートをテストする意図がない限り、1024 を大きく下回るサムネイルに合わせないでください。極端に狭い幅でレンダリングすると、同じマークアップでもデスクトップ幅では発生しないclipped-textの検出結果が生じます。 - ページが折り返し位置を超えて続く場合は
--full-pageを指定します。 長いフォーム、縦に積み重なったレポートなど、スクロールを前提とするページが該当します。スクロール可能なページ全体を 1 枚の PNG に結合します。 - ページが垂直方向の flex シェルの場合は
--heightを指定します。 ルート領域がvflexのページは、構造上ビューポートと同じ高さになるため、--full-pageを指定しても表示範囲は広がりません。代わりに--heightを大きくしてください。hflexは幅に関するもので、このような動作にはなりません。hflexのページはほかのページと同様に折り返し位置を超えて続くため、その場合は--full-pageが適切なフラグです。SIZE:のビューポートとまったく同じ高さで返された--full-pageのキャプチャは、ページが切り詰められたのではなく、flex でサイズが決まっていることを示しています。
uv run ~/.claude/skills/zul-writer/scripts/preview-zul.py --width 1600 --full-page --run-controllers src/main/webapp/index.zul
スクリプトはプロジェクトの ZK jar(Maven、Gradle、またはどのプロジェクトにも属さないファイルの場合は標準の ZK)を解決し、ZK 独自のエンジンでページをレンダリングして PNG を書き出します。Java 17 以降と Google Chrome または Microsoft Edge が必要です。 初回の実行時にレンダリング用ヘルパー(zk-preview-launcher.jar、約 500 KB)をダウンロードして SHA-256 を検証し、~/.cache/zul-writer/ にキャッシュします。2 回目以降の実行にはネットワーク接続は不要です。
次に、画像読み取りツールで PNG を読み取り、Step 1 の回答と比較してください。ユーザーがスクリーンショットまたはモックアップをもとに作業を始めた場合は、その画像も再度読み取り、2 つを並べて比較してください。
プレビューがない場合
スクリプトは 2 で終了し、PREVIEW_SKIPPED: で始まる 1 行を出力します。これは、解決可能な ZK jar がない、Java 17 以降がない、ブラウザーがない、またはヘルパーをダウンロードできなかった場合に発生します。これは ZUL の不具合ではありません。 1 行で報告し、「レンダリング済みプレビューをスキップしました:<理由>」と記載して、通常どおりタスクを完了してください。
見ていないスクリーンショットについて説明してはいけません。また、スキップされたプレビューを、プレビューに合格したものとして扱ってはいけません。
NEXT: 行には、プレビューを可能にするために必要なことが記載されています。ここで解決できそうな場合は、再試行を1 回だけ行ってください。--debug を追加すると、解決されたクラスパス、すべてのヘルパーコマンドライン、レンダラー自体の出力が stderr に表示されます(stdout は変わりません)。通常はこれで原因を把握できます。その後は作業を打ち切り、いずれの場合もスキップしたことを報告してください。
最初にCONTROLLERS:行を確認してください
出力内の1行で、以下の判定ルールはこの行によって逆転します。
| 行 | 画像の状態 | 読み取り方 |
|---|---|---|
CONTROLLERS: executed | コントローラーが実行されています。実際のバインド値、実際のモデル行、実際のコンポーザー出力です | 判定できないことが少なくなります。バインドされたフィールドが空欄なら、不具合です |
CONTROLLERS: skipped (isolated) | デフォルトの状態です。ComposerもViewModelもありません | 暗く表示された式テキストやプレースホルダー行は、正しい動作です |
CONTROLLERS: failed → isolated | コントローラーを要求しましたが失敗し、代わりに分離レンダリングが表示されています | 分離時のルールに従って確認し、以下に追加された修正すべき点の箇条書きも参照してください |
まず LAYOUT: ブロックを確認してください
ブラウザーで測定した結果なので、これは意見ではなく事実です。PNG を開く前に確認してください。
各行は rule | locator | measurement の形式です。
LAYOUT: 3 findings
- zero-size | a[label="Settings"] | 0x0 with text but no box
- clipped-text | a[label="Documents"] | text needs 77px, box is 48px
- viewport-overflow | grid.gp-wide | page scrollWidth 2005 > viewport 1280; widest offender 2000px
ロケーターには、コンポーネントに ZUL id がある場合はその id(label#breadcrumbCurrent)を、それ以外の場合はコンポーネントと識別用の属性(a[label="Settings"])、またはスタイルクラス(grid.gp-wide)を指定します。生成された id が指定されることはないため、常に自身のマークアップ内で見つけられます。
| ルール | 意味 | 修正方法 |
|---|---|---|
clipped-text | テキストがクリップされるボックスに収まらず、一部が切れている状態です | ボックスを広げる(width/hflex)、折り返しを許可する、またはテキストを短くします |
zero-size | コンポーネントがまったく領域を占めておらず、内容が見えない状態です | コンポーネントまたはその祖先に height/vflex がないか、width: 0 のスタイルルールがあります |
escapes-parent | 表示内容がクリップされる祖先要素からはみ出し、そのはみ出した部分が切れている状態です | 親要素に余裕を持たせるか、子要素がはみ出さないようにします |
viewport-overflow | ページの幅がビューポートより広く、横スクロールバーが必要な状態です。行には最も幅の広い要素が示されます | 指定された要素の固定幅を削除するか、hflex/パーセンテージを使います |
icon-not-rendered | フォントアイコンが空のボックスとして描画される状態です。グリフはありますが、ブラウザーが解決したフォントスタックではそのグリフを表示できません | アイコンのフォントを維持できるキャリアにアイコンクラスを設定します。iconSclass または通常のコンテナーを使い、独自の font-family が優先される要素には設定しないでください |
literal-rows-discarded | マークアップに記述された行がページに表示されていない状態です。モデルを設定するとその行が破棄されるためです。通常は、データを移動したものの古い行を残した抽出処理が原因です | リテラルの <listitem>/<row>/<treeitem> 要素を削除します。現在、ページの行データのソースはモデルのみです |
literal-rows-discarded は、ページに含まれていないものについてのルールなので、PNG で確認できない唯一のルールです。ページは正しくレンダリングされます。モデルの行はすべて表示されている一方で、マークアップには何も表示されない行が残っています。その行を次の編集者が変更しても何も変わらず、理由もわからないままです。このルールは 2 通りの方法で検出されます。マークアップだけの場合は、model 属性とリテラルの行が並んでいると検出されます。レンダリング時の場合は、Composer の setModel()(Java にあるため、ZUL のチェックでは確認できません)が行を置き換えていると検出されます。ページング、折りたたまれたツリーノード、未選択のタブでは、正当に行がページに表示されません。このルールはそれぞれの状態を考慮して測定するため、ここでの検出結果はそのいずれにも該当しません。
このブロックを確認する前に、次の 3 点を把握しておいてください。
- 報告するものがなければ、ブロック自体が出力されません。
LAYOUT:行がなければ、監査は実行され、問題は見つかっていません。 - このループでは、終了コードは変わりません。 検出結果は報告されるだけで、強制されません。
STATUS: okは引き続きページがレンダリングされたことを意味します。検出結果を終了コード 4 にするのは CI の--fail-on-layoutだけで、その場合でも先にSTATUS: okが出力されます。 - キャプチャした画像だけでなく、ドキュメント全体が対象です。
--full-pageの指定の有無にかかわらず、スクロールしないと見えない部分も含まれます。そのため、スクリーンショットには写っていない要素が示されることがあります。測定結果を信頼し、実際に確認したい場合は--full-pageを指定して再レンダリングしてください。
CONTROLLERS: skipped (isolated) の下にある clipped-text の検出結果がプレースホルダーテキスト(狭い列にある prod.price)に関するものであれば、測定対象は実際のデータではなくプレースホルダーです。そのために列を広げる前に、--run-controllers を指定してレンダリングし、確認してください。
STATE: — 出荷された状態の折りたたみ
このブロックにある内容はどれも不具合ではないため、LAYOUT:には含まれていません。グループボックスが閉じた状態で表示されているのは、デザインどおりの場合もあれば、その逆の場合もありますが、ページを見てもどちらかは判断できません。意図された状態はモックアップか、ユーザーの依頼内容にしかありません。そのため、レンダリング結果には状態を一覧表示し、デザインと照らし合わせます。
STATE: 3 collapsible (1 open, 2 closed)
- open | groupbox#gbFilters
- closed | groupbox[title="Advanced"]
- closed | treeitem#tiSettings
デザインと照らし合わせ、1行ずつ確認してください。 このブロックがある理由はこれに尽きます。見た目で折りたたみ状態を判断するには、画面に表示されていないものに気づく必要がありますが、そこは見落とされがちな確認項目です。ある評価実行では、モックアップでは折りたたまれていたサイドバーのツリーが、完全に展開された状態で出荷されましたが、何も指摘されませんでした。短い2つのリストなら、スクリーンショットと記憶を照らし合わせるより、比較しやすくなります。
一覧に表示されるコンポーネントは4つです。groupbox、nav、detail、treeitem — ページが指示どおりに動作していても、その内容が非表示になる可能性のあるすべての要素です。子を持たないtreeitemは、折りたためないため一覧から除外されます。意図的に除外されているのは、選択されていないタブ、ページ分割されたグリッド、スクロール可能な領域です。これらも内容を非表示にしますが、それが本来の役割です。一覧に含めると、誤った状態で出荷される4つの要素が埋もれてしまいます。
この情報を確認する前に、知っておくべきことが3つあります。
- 閉じた項目の子孫要素も、このブロックには表示されません。 ZKは折りたたまれたブランチの子要素をまったくレンダリングしないため、閉じたブランチの中にある
treeitemは一覧に表示されません。まだ存在しないからです。表示されるのは、現在ページ上にあるすべての要素の状態であり、ユーザーが見られるものもそれだけです。 LAYOUT:と同様、ページ上に折りたためる要素がない場合は、ブロック全体が省略されます。 ブロックがなければ、確認する必要もありません。- ここでの差異は実際の不具合であり、修正には
open属性を使います — 属性を1つ変更するだけで、レイアウトは変わりません。デザインにも依頼内容にも、コンポーネントがどの状態であるべきか記載されていない場合は、そのままにして、その旨を伝えてください。誰からも指示されていないopen属性を変更する必要はありません。
WARNINGS: — コンソールとクライアントのエラー
WARNINGS: ブロックに含まれる3種類のエントリーは、ランチャーではなくブラウザーに由来します。
| エントリー | 意味 |
|---|---|
console error: <text> | ページ自身の JavaScript が console.error を呼び出しました。<zscript>、n:script、またはウィジェットのクライアントコードが該当します |
console warning: <text> | 同様に console.warn が呼び出されました。ZK のクライアントエンジンは、実際にいくつかの警告を出します(ロケールが読み込まれていない場合や、AU バッチが想定外に大きい場合など) |
ZK client error: <text> | ZK のクライアントエンジンが問題を報告しました。不明なウィジェット、マウントの失敗、Mold の欠落などです。サーバー側では問題がなく、STATUS: ok、終了コードは 0 です |
真剣に受け止めるべきなのは ZK client error: エントリーです。これは、サーバーがレンダリングしたページがブラウザー上で実際には表示されなかったことを示す唯一のシグナルで、終了コードには現れません。
知っておくべき制限が3つあります。
- 対象となるのは初回描画のみです — スクリーンショットの直後までが対象です。それ以降の AU 往復で発生したエラーは検出されないため、ブロックが空でも、セッションに問題がなかった証明にはなりません。
- ZK のクライアント側のエラーはページ上のエラーボックスに表示されます。ZK はエラーをコンソールに出力しないため、スクリプトは DOM からこのボックスを読み取ります。これはページに追加される実際の要素で、上部中央に表示される「N Errors」という見出し付きの薄赤色のパネルで、再読み込みアイコンと閉じるアイコンがあります。そのため、通常はPNG にも表示されます。画像に見慣れない赤いパネルがあっても、それは同じエラーを示しているのであり、修正が必要なマークアップではありません。パネルの
Nは発生したメッセージをすべて数えますが、WARNINGSのエントリーは重複が除かれるため、同じエラーが繰り返された場合、両者の数が異なるのは正常です。 - ブラウザーの
Failed to load resource:行は意図的に報告されません。 どのページでもファビコンがない場合にこの行が1つ出ますが、URL が含まれないため、残しておくと問題のないすべてのページにエラーが記録されます。実際に読み込みに失敗したものには、その URL を示す個別のWARNINGSエントリーが出ます。~./のクラスパス上で見つからない場合はZK resource not served、ドキュメントルートにないファイルの場合はpage asset not foundです。
ページの動作に問題があるのにブロックが空の場合は、--debug を使ってください。このオプションは、ブロックで除外されるレベルを含め、すべてのコンソールレベルを stderr に出力します。
読まずにスクリプトで処理する場合
--report json[:<path>] を指定すると、この実行結果が1つの JSON オブジェクトとして書き出されます。デフォルトでは PNG と同じ場所に .json 拡張子で保存されます。これにより、CI やコーパスのジョブでこれらの行を解析する代わりに、構造化された実行結果を比較できます。stdout に追加されるのは REPORT: <path> のみで、上記の各ブロックは変更されません。スキーマの全体像と、終了コードごとに値が設定されるキーについては、references/preview-guidelines.md を参照してください。
ステップ5を手作業で行う場合は、--report を指定しないでください。 スクリプトを実行したのと同じツール呼び出しで、すでにこの stdout を取得しているため、ファイルを作成しても同じ情報をもう一度取得するだけです。
修正する内容
ピクセルやデータではなく、構造を確認してください。修正するのは次の項目だけです。
- ページの代わりにエラーページが表示される(スクリプトが終了コード 1 で終了し、
PHASE、MESSAGE、LOCATIONを出力する)。実際の ZUL の不具合です。報告された場所を修正してください。 - 「Unknown component
<x>」 — そのコンポーネントを定義する jar がクラスパスにありません。タグの誤記か、アドオンの依存関係が不足しています。コンポーネントを削除せず、ユーザーに確認してください。 - ステップ 1 で求められた内容と比べて、セクションが不足している、または余分にある。
- 領域の配置が誤っている — サイドバーがコンテンツの下に表示される、ヘッダーがない、タブの内容が
<tabpanel>の外にある。 - コンポーネントの選択が誤っている — データテーブルがラベルの単なる縦並びとして表示される、フォームフィールドが要求された入力タイプではない。
- レイアウトの崩れ — コンテンツが切り取られる、はみ出す、収まるはずのページに横スクロールバーが表示される、領域の高さがゼロに縮む、ウィジェットが重なる、
hflex/vflexが見た目に反映されていない。LAYOUT:ブロックには、これらすべてが該当コンポーネントと測定値とともに正確に記載されています。目視で判断する項目より先に修正してください。 - ZK コンポーネントを使うべき箇所に、スタイルの適用されていない HTML がそのまま表示される。
- ページで要求されたアセットが読み込まれていない。 プレビューサーバーは docroot を配信するため、画像、スタイルシート、スクリプトが見つからないことを示す
WARNINGSの行は、そのパスにファイルが存在しないことを意味します。これは修正する価値のある実際の欠落であり、プレビューの制限ではありません。まずパスを確認し、次にDOCROOT:の行を確認してください。docroot を推測すると、正しいパスが誤っているように見えることがあります。~./URL は別の形式です。これはクラスパスに jar がないことを意味するため、マークアップを編集するのではなく、依存関係についてユーザーに確認してください。 - フォントアイコンが空の四角として表示される —
LAYOUT:にicon-not-renderedとして報告され、グリフと、それを提供できなかったフォントスタックが記載されます。アイコンフォントを上書きする carrier にクラスが付いています。references/ui-to-component-mapping.md の Icons に、クラスを移すべき carrier が記載されています。これはマークアップの単語を 1 つ直すだけの修正です。評価では測定される前に、別々の 3 通りの誤診がありました。ウェブフォントについての推測ではなく、この指摘を信頼してください。 - 未知のウィジェットまたはマウントの失敗を示す
ZK client error:の項目 — サーバー側のクラスは解決されているものの、アドオン jar のクライアント側 JS パッケージがありません。そのためページは解析されても、正しく表示されません。WARNINGSの 404 項目とクラスパスを確認し、依存関係についてユーザーに確認してください。正常に動作するマークアップを書き換えてはいけません。 LAYOUT:のliteral-rows-discarded— 指摘されたリテラル行を削除してください。これは画像を見ても確認できない唯一の指摘です。画像に表示されているページは正しい一方、表示されないマークアップに不具合があります。ほとんどの場合、抽出時にデータだけが移動され、古い行が残っています。削除する前に、コントローラーが実際にそのデータを提供していることを確認してください。- 設計と食い違う
STATE:の行 — モックアップでは閉じている groupbox、nav、detail、ツリーのブランチが開いた状態で表示される、またはその逆です。修正するのはopen属性だけです。設計にもリクエストにも状態の指定がない場合は、そのままにして、その旨を伝えてください。 CONTROLLERS: failed → isolated—WARNINGSの項目を読み、失敗したクラスと最初の原因の行を確認してください。コントローラーの例外、クラスの欠落、予算超過は、ZUL ではなくコントローラー(またはビルドの欠落)の不具合です。コントローラーを修正して再レンダリングするか、問題を報告してください。マークアップに値をハードコードして回避してはいけません。終了コードは 0 のままで、スクリーンショットも有効です。ただし、そこに表示されているのは分離された状態のレンダリングです。
画像に不具合は映っていても、原因がわからないとき
アイコンがあるはずの場所に空の枠がある。コンポーネントが見つからない。誰も求めていない色になっている。設定したものとは違う幅になっている。どの場合も、PNG は何かがおかしいことを示していますが、その理由は何も教えてくれません。そして再レンダリングしても同じ PNG が生成されます。
再レンダリングする代わりに、要素を調べます。 --probe '<css-selector>' を指定すると、ブラウザーが実際に構築した状態で、該当するすべての要素について、開始タグ、測定したボックス、不具合につながる計算済みスタイルが報告されます。すでにあるレンダリング結果を読み取るため、追加の処理は不要です。
uv run --with playwright preview-zul.py page.zul --probe '[class*="z-icon-"]'
LAYOUT: の検出結果には、要素を特定する CSS ロケーターがすでに記載されています。そのロケーターをそのまま --probe に貼り付ければ、測定結果がその値になった理由を確認できます。何を調べればよいかまだわからないときは、--dump-dom でマウント後の DOM 全体をファイルに書き出して検索できます。データ量の多いページでは数百 KB に及ぶため、ブロックではなくファイルに書き出します。
これは、ZK ページがマークアップとして存在する唯一の場所が DOM だからです。配信されるレスポンスは zkmx([...]) というブートストラップにすぎず、.zul をそのまま書き戻しています。クラス名、フォント、ボックスはすべて、その後ブラウザー内で構築されます。
この画像から判断できないこと
プレビューでレンダリングされるのは初回描画のみです。CONTROLLERS: skipped (isolated) または
failed → isolated の場合、ViewModel も Composer も実行されず、以下はすべて
レンダラーが正しく動作した結果です。これを「修正」したり、問題として報告したり、再レンダリングの理由にしたりしないでください。
- バインドされた値が薄い式テキストとして表示されること(例:テキストボックス内のリテラルな
vm.customer) — ViewModel は実行されません。これはレンダラーの正しい動作です。また、新しいモデル駆動ページをまずリテラルなデータで記述する(ステップ 2)のもこのためです。実際に表示されるテキストではないテキストを使ってレイアウトを評価することはできません。 modelがバインドされている<grid>/<listbox>/<tree>のプレースホルダー行(例:each.productと表示される行) — 薄く表示されたサンプル行によって、コンポーネント本来の形状が保たれます。- バインドされた
srcを持つ<include>がある箇所でセクション全体が欠落すること — これはプレースホルダーにはなりません。定数のリテラル(src="@load('~./page.zul')")は実際にインクルードされますが、ViewModel が提供する値(src="@load(vm.page)")ではsrcが未設定のままになるため、インクルードは何も追加せず、薄いテキストではなく空白が表示されます。空白を「修正」するためにハードコードのsrcを追加すると、実際のページが壊れます。 - Composer または ViewModel が設定するあらゆるもの — デフォルト値、初期選択、計算されたラベル、i18n テキスト。
apply="..."の Composer はここでは何もしません。 - サーバーとの往復が必要なあらゆるもの — ボタンクリック、ページング、並べ替え、ツリーの展開、選択状態の強調表示、イベントによって開かれる
<window>やポップアップ。存在するのは初回描画の状態だけです。クライアント側のw:ハンドラーと<zscript>は実行されます。 - テーマの jar がプロジェクトの依存関係にない場合の、テーマに依存する色や余白。
- 正確な余白、フォントのレンダリング、サブピクセル単位の配置、またはモックアップとわずかに異なる色。
- モックアップのデータ内容 — サンプルデータは異なります。値ではなく、UI の構成を比較してください。
--full-page画像の高さ。--full-pageは閲覧コンテキストのサイズを変更しません。Playwright があとから縦長の PNG をつなぎ合わせるため、LAYOUT:の検出結果とすべてのhflex/vflexの測定値は画像の高さではなくSIZE:のビューポートを基準にしています。references/preview-guidelines.md のビューポートを参照してください。--full-pageキャプチャにおけるposition: fixed要素の位置。要素はビューポートに固定されますが、つなぎ合わせた画像はビューポートより縦長です。そのため、bottom: 40pxと記述されたフローティングアクションボタンはSIZE:の高さから 40px 上に配置され、画像の中央に取り残されたように見えます。ページは正しく、キャプチャのほうに問題があります。測定例:高さ 900 のビューポートでは、bottom: 40pxの FAB は高さ 1277 の PNG の y=798 にレンダリングされました。これを追いかけて修正せず、別の要素を動かして「修正」することも避けてください。ビューポートと同じサイズでレンダリングして確認してください。
CONTROLLERS: executed の場合、上記の最初の 4 項目は該当しません — 薄い式テキスト、プレースホルダー行、バインドされた src のセクションの欠落、「Composer または ViewModel が設定するあらゆるもの」は、すべて実際の出力になります。このモードでは、バインドされたフィールドが空欄のまま、データテーブルに行がない、またはセクションが欠落したままであれば、不具合です。値が提供されていないなら Controller に、バインディング先のプロパティ名が間違っているなら ZUL に問題があります。*「サーバーとの往復が必要なあらゆるもの」*以降の項目は、どちらのモードでも引き続き該当します。
空状態のページには初回描画が2通りあり、そのうち1つしかレンダリングしていません
empty vm.something によって表示が切り替わるもの(マスターディテールペイン、「結果がありません」というメッセージ、ダイアログの本文など)には、初回描画では到達しないもう1つのレイアウトがあり、欠陥はそちらに偏って潜みます。誰も見ない側だからです。
このスキル自身の assets/master-detail-mvvm.zul には、そうした欠陥が2つありました。検証は問題なく通り、レンダリングも問題なく見えましたが、どちらも選択済みの状態に潜んでいました。詳細ペインは vm.selectedItem.description にバインドされていましたが、対応する MyViewModel.Item にはそのプロパティがありませんでした。これは空白で表示されるのではなく、PropertyNotFoundException を発生させてコントローラー全体を停止させるため、ページは CONTROLLERS: failed → isolated にフォールバックし、プレースホルダーを表示します。また、visible は hflex="2" のペイン自体に設定されていたため、最初のクリックまではペインの幅がマスターリストに移り、その後は戻っていました。何も選択していないときは1265px、行を選択すると422pxと計測されました。静的解析のどのレイヤーでも、初回描画でも見つけられない、3倍の急変です。
2回目の描画を行うには、コントローラーに1行加えるだけです。行を選択した状態から開始し(@Init で selectedItem = items.get(0))、レンダリングしてから元に戻します。リストが空で返される可能性があるページ、ステップ1を過ぎたウィザード、折りたたまれた状態で始まるパネルでも同じようにしてください。2つの状態でペインの形状を比較し、内容だけで判断しないでください。コンテナーで --probe を使うと幅が報告されます。状態によって幅が変わるなら、それが欠陥です。
ラウンド数
1ラウンドは、レンダリングし、確認し、修正対象から欠陥を列挙し、修正して、ステップ3の検証を再実行することです。次のラウンドが必要かどうかは3つのルールで判断します。それぞれ、ほかのルールでは見つけられない問題を検出します。
- 1つの不具合に対する修正は最大2回までです。 不具合を直すための2回目の編集後も残っている場合は、それ以上編集しないでください。何も変わらない編集が2回続いたなら、想定している原因は原因ではありません。3回目の編集では不具合ではなくマークアップを変更することになり、変更されるのは正常に動作していた部分です。代わりに原因を特定するか(後述のとおり、これは修正ラウンドに数えません)、不具合を報告してください。計測結果を持ち帰って再検討するのは、3回目の推測ではありません。このルールは、1つの不具合に対して仮説を変えながら連続して何度も編集する、盲目的なループを防ぎます。
- 不具合の一覧が減っている間は続けてください。 不具合を少なくとも1つ解消し、新たな不具合を持ち込まなかったラウンドに限り、次のラウンドに進めます。新たな解消がないラウンドがあれば、後述の診断ルールと同じく、そこで止めてください。計測する段階では件数を覚えておく必要はありません。レンダリングするたびに
LAYOUT: N findingsと表示されます。 - 1パスあたりの修正ラウンドは最大4回です。 前述の2つのルールでは防げないケース、つまり不具合が入れ替わり、各ラウンドで1つ解消する一方で別の不具合が生じるケースに備える上限です。計測を含む6回の実行で、3回を超える修正が必要なページはありませんでした。そのため、4回に達したこと自体が問題です。黙って止めるのではなく、その旨を伝えてください。パス1(リテラルなレイアウト)とパス2(抽出後)には、それぞれ別に4回の上限があります。抽出後に表面化した不具合の原因は抽出であり、レイアウト上の不具合とは異なるためです。
- 上限の対象は編集回数であり、レンダリング回数ではありません。 何が間違っているのか(どの要素か、どの計測値か、考えられる2つの原因のどちらか)を調べるために再度レンダリングしても、修正ラウンドには数えず、回数の上限もありません。同様に、
--probeや--dump-domの実行も上限の対象外です。まだ何も変更していないので、同じ間違いを2回したことにはなりません。これらのルールが防ごうとしているのは、推測で編集し、次の推測でまた編集することです。原因が分からないまま止めると、実際の不具合がそのまま出荷されたり、プレビューのせいにされたりします。診断には別の停止ルールがあり、回数では決まりません。次のレンダリングで、前回は分からなかったことが何も分からない時点で止めてください。 ある評価では、すべてのスクリーンショットをクリップしていたチャートのアニメーションを特定するために6回レンダリングしましたが、それは適切な判断でした。上限を設けて違反のように感じさせるとしたら、測る対象を間違えています。 - 一覧にある不具合だけを、全体として解消してください。 一覧が空の場合、または残っている項目がすべて 判断できないこと に該当する場合、ページは十分な状態です。その旨を1行で伝えて終了してください。「十分な状態」とは、ステップ1の要件がすべて目に見える形で満たされ、適切な領域に適切な種類のコンポーネントとして配置され、クリップや重なりがないことを意味します。
- モデル駆動型のページでは、パス1の修正ラウンドをリテラル版に使ってください。 データがまだマークアップ内にある間にレイアウトを確定し、その後抽出して
--run-controllersを付けて1回レンダリングしてください。このレンダリングは修正ラウンドではありません。レイアウトではなく抽出を確認するためのものです。その後ページの見た目が変わった場合は、変更したのは抽出です。そこで見つかった問題はすべてパス2に属し、パス2の上限内で修正します。 - 見た目の違いだけを理由にZULを編集しないでください。 モックアップにピクセル単位で合わせようとすると、修正ラウンドを消費し、正常に動作していたマークアップを後退させます。
- ラウンドによってページが開始時より悪くなった場合は、その編集を元に戻して停止してください。 何かを後退させた修正は、まだ不具合の原因を理解できていない証拠です。すでにあるページは、次の推測よりも価値があります。
- 不具合が残った状態で停止する場合は、その内容と試したことをユーザーに伝えてください。 それ以上レンダリングしないでください。
- 修正ラウンド数と、まだ残っている問題を伝えてください。 予算について誰にも伝えなければ、後から確認できません。これはステップ1で使用したデフォルト値を明記するのと同じ理由です。ユーザー自身がページを確認できるよう、最終画像のパスも報告してください。
クラスパスの解決方法、docroot のルール、および各 PREVIEW_SKIPPED の理由については、references/preview-guidelines.md を参照してください。