本文へスキップ

Runtime Protocol

このページは英語から翻訳されています。 英語のページを読む

ステータス: ドラフト。zeyn::server と assets/zeyn/*.js(/_zeyn/zeyn.js として配信)が実装しています。

これは Zeyn サーバーとブラウザランタイムの間の契約です。Screen はサーバー側でレンダリングされます。ブラウザランタイムは小さなスクリプトで、ユーザー操作をサーバーへ送り、返ってきた結果を適用します。アプリケーションコードが JavaScript を書くことはありません。

1. Page load

GET {route}?params={json} は完全な HTML ドキュメントを返します。params は Screen params の URL エンコードされた JSON で、Screen に params がない場合は省略されます。

/orders/{id} のようにパスセグメントを持つルートでは、params のうちそのフィールドをパス(/orders/42)から、それ以外のフィールドを params から読み取ります。解釈できないセグメント値は 404 を返します。値はドットも含めてパーセントエンコードされる(%2E)ため、Screen の URL がランタイムにファイルとして扱われることはありません。Screen::moved_to を返す Screen には、新しい URL を Location に入れた 301 Moved Permanently が返ります。

失敗したページは、そのステータス(404、403、500 など)と、アプリのエラー Screen(App::not_found、App::server_error)または組み込みの短いメッセージをシェル内に表示して返します。x-zeyn-partial: 1 を付けたリクエストには、その Screen だけが x-zeyn-partial: 1 と x-zeyn-title を付けて返るため、ランタイムはページを読み込み直さずに差し替えられます。エラーの詳細がブラウザに届くことはありません。

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <meta name="zeyn-csrf" content="TOKEN">
  <title>Products · Inventory</title>
  <link rel="stylesheet" href="/_zeyn/zeyn.css?v=BUILD">
  <script type="module" src="/_zeyn/zeyn.js?v=BUILD"></script>
</head>
<body>
  <a class="z-skip" href="#z-main">Skip to content</a>
  <div class="z-app">
    <nav class="z-nav" aria-label="Main">...</nav>
    <main class="z-main" id="z-main">SCREEN</main>
  </div>
  <div class="z-toasts" role="status" aria-live="polite"></div>
</body>
</html>

メニューには、現在のユーザーが開ける Screen だけが並びます。現在の項目には aria-current="page" が付きます。

head にあるフレームワークのスクリプトとスタイルシートにはすべて、?v= とビルド ID(meta[name=zeyn-build]。ZEYN_BUILD_ID か実行ファイルのハッシュ)が付き、theme.css にはテーマ文字列のハッシュが付きます。現在の値を持つリクエストは Cache-Control: public, max-age=31536000, immutable を受け取るため、ウォームなページ読み込みではそれらを 1 つも要求しません。それ以外のリクエスト(デプロイ後の古いタブ、v なし)は no-cache と ETag を受け取ります。ランタイムがあとからインポートするモジュールは、それをインポートするスクリプトの URL から同じ v を引き継ぎます。

すべてのページが zeyn.js(リクエスト、effect、ウィンドウ、フォーム、メニュー、ショートカット、イベント委譲)、nav.js、components.js を読み込み、アプリシェルでは pro.js と enterprise.js も読み込みます。残りは、それを使うページでだけ読み込まれ、nav.js が画面を差し替えるときも同じです。zeyn.js は、コンテンツに [data-zeyn-grid] が初めて入ったときに grid ランタイム grid.js をインポートし(grid を持つ完全なページは <link rel="modulepreload"> でこれを指定します)、components.js は catalog.js(カード grid、検索ボックス)、media.js(プレーヤー、埋め込み)、files.js(アップロード)、charts.js、assistant.js(チャットパネル、またはランチャーを初めて押したとき)をインポートします。これらのスタイルシートは head に残るので、スクリプトが届いても見た目は変わりません。showcase のダッシュボードでは、9 スクリプト 403 KB(116 KB)ではなく、5 スクリプト 258 KB(転送 74 KB)になります。

2. Screen markup

すべての Screen のルートは、名前と params を持ちます。

<section class="z-screen" data-zeyn-screen="product_list" data-zeyn-params="null" aria-labelledby="product_list-title">

Window は class="z-window" role="dialog" aria-modal="true" と --z-width スタイルを使います。

body の各項目はノード要素で包まれます。そのパスが、以下のエンドポイントで使うアドレスです。コンテナ(Row、Stack、Panel、Card、Tabs、Split)も各子を同じ方法で包み、子のパスにはインデックスが加わります。body 項目 1 の最初の子の 3 番目の子は "1.0.2" です。

<div class="z-node" data-zeyn-node="1">
  <div class="z-node" data-zeyn-node="1.0">...</div>
</div>

エンドポイントは node を、ドット区切りの文字列、または body 項目の場合は単純な数値(0)として受け取ります。パスは数字で最大 16 階層までで、それ以外は 422 で拒否されます。

3. Attributes the runtime reads

属性付与先意味
data-zeyn-effectbutton、trクリック時に実行する JSON の effect(行ではダブルクリックまたは Enter)
aria-keyshortcutsbuttonキーボードショートカット(例: Control+N)
data-zeyn-exportbutton同じ Screen 上の grid のエクスポート形式(csv、xlsx)
data-zeyn-gridgrid ルートgrid を示します。data-zeyn-mode、data-zeyn-paging、data-zeyn-page-size、data-zeyn-total、data-zeyn-next、data-zeyn-offset も付きます
data-field、aria-sort、aria-colindexth列のフィールド名、現在のソート、位置
data-filter、data-optionsthフィルター入力の種類(text、number、money、date、datetime、time、choice、bool)と、選択肢 [{value,label}]
data-pinth固定列の start または end(選択列と行アクション列も含む)
data-edit、data-optionalth保存できるユーザー向けの、編集可能な列の入力の種類
data-zeyn-texts、data-zeyn-columns、data-zeyn-viewgrid ルートローカライズされたコントロールのテキスト、全列 [{field,label,hidden,pin,width}]、保存されたビュー
data-zeyn-viewsgrid ルートビューをサーバーに保存する場合は server
data-zeyn-version-field、data-zeyn-exportablegrid ルートインライン編集用の行バージョンのフィールド名。grid をエクスポートできる場合は csv
data-zeyn-key、data-zeyn-version、data-zeyn-menutr行キー、行バージョン、コンテキストメニューの項目
data-vtd編集可能なセルの入力値
data-zeyn-detail、aria-expandedtr{screen, params} 形式の行の詳細 Screen、および開いているかどうか
data-zeyn-expand行内の button行の詳細、グループ、子を開閉します
data-zeyn-level、aria-level、data-zeyn-pathtreegrid 内の trグループ化または tree の grid での深さ(最上位が 0)。行を開くときに送る、グループ値または祖先キーのパス
z-grouptrグループヘッダー行。z-branch-more は枝の「もっと読み込む」行
data-zeyn-run行内の button実行する行の項目(a0 は最初の行アクション)
data-aggtfoot td列が表示する集計(sum avg min max count)
data-zeyn-bulkgrid の前の divdata-zeyn-bulk-run ボタンを持つ選択バー
data-zeyn-menuScreen ルートScreen のコンテキストメニュー
data-zeyn-native-menu="off"htmlテキスト入力以外ではブラウザのコンテキストメニューを無効にします
data-zeyn-rowstbody行を入れる場所
data-zeyn-pageページャーの buttonprev または next
data-zeyn-page-infospan1-50 of 230 のようなテキスト。grid ルートのローカライズされたテンプレートから作られます
data-zeyn-text-range、data-zeyn-text-range-total、data-zeyn-text-loadedgrid ルートローカライズされたテンプレート: {from}-{to}、{from}-{to} of {total}、{count} rows loaded
data-zeyn-morediv無限スクロールのセンチネル。「もっと読み込む」ボタンを含みます
data-zeyn-submitformsubmit action の名前
data-zeyn-editorinputカスタムフィールド型のクライアント側エディター

Effects:

EffectJSONランタイムの動作
Open window{"effect":"open_window","screen":"product_form","params":{...}}Screen(セクション 4.1)をモーダルの <dialog> に読み込みます
Navigate{"effect":"navigate","screen":"...","params":{...}}/_zeyn/route/{screen}?params= を読み込み、ページへリダイレクトします。ルートにセグメントがある場合はパス形式になります
Toast{"effect":"toast","message":"Saved"}トーストを 4 秒間表示します
Close and refresh{"effect":"close_and_refresh"}最前面のダイアログを閉じ、その下の Screen にあるすべての grid を再読み込みします

4. Endpoints

すべての POST リクエストは JSON を送り、zeyn-csrf meta タグのトークンを入れたヘッダー x-zeyn-csrf を必ず付けなければならず、付いていない場合は 403 で拒否されます。

セッションなしで開かれた public Screen は、キャッシュできるように、空の zeyn-csrf meta タグを持ち、Set-Cookie も付きません。最初の POST の前に、ランタイムは GET /_zeyn/csrf を呼び出します。これは { "token": "..." } を Cache-Control: no-store 付きで返し、ブラウザに zeyn_csrf Cookie がなければ設定し、Sec-Fetch-Site が same-origin でないリクエストは拒否します。トークンは meta タグに入ります。呼び出しが失敗した場合、POST はトークンなしで送られ、ほかのリクエストと同様に拒否されます。黙って再試行することはありません。

アプリ内ナビゲーション(nav.js)は、x-zeyn-partial: 1 を付けて Screen を要求し、HTML だけを受け取ります。ページタイトルは x-zeyn-title に入ります。Screen が head タグ(description、canonical URL、プレビュー画像、robots、JSON-LD)を設定する場合、body はそれらを収めた <template data-zeyn-head> で始まります。nav.js はドキュメントの head にある [data-zeyn-head] 要素をすべて削除し、template のタグをそこへ移し、template を破棄します。head タグのない Screen は template を送らないため、古いタグは削除されるだけです。

4.1 Screen fragment

GET /_zeyn/screen/{screen}?params={json} は、ページシェルなしで Screen の HTML だけを返します。Window と行の詳細で使われます。data-zeyn-detail を持つ行を開くと、その Screen が直下の新しい tr.z-detail(行全体にまたがる 1 つのセルで、role="region" を持つ)に読み込まれ、閉じるとその行が削除されます。Screen の権限はほかの Screen と同じように確認されます。詳細内の grid は、その詳細 Screen 自身の名前と params でサーバーとやり取りします。

4.2 Grid rows

POST /_zeyn/grid

{
  "screen": "product_list",
  "params": null,
  "node": 0,
  "query": {
    "page": { "type": "offset", "offset": 0, "limit": 50 },
    "sort": [{ "field": "code", "desc": false }],
    "filters": [{ "field": "name", "contains": "tea" }, { "field": "price", "from": "10", "to": "99.90" }],
    "with_total": true,
    "aggregates": [{ "field": "price", "op": "sum" }]
  },
  "view": { "columns": [{ "field": "code", "width": 140, "pin": "start" }, { "field": "name" }] },
  "head": false
}

フィルターには contains、starts_with、equals、from、to、any_of、is を設定できます(docs/grid.md を参照)。view.columns は、行をレンダリングするときの列レイアウトです。head: true の場合、レスポンスにはヘッダー行の head も含まれます。

無限スクロールでは、page は { "type": "after", "cursor": "...", "limit": 100 } で、最初のバッチでは cursor は null です。

レスポンス 200:

{ "rows": "<tr>...</tr>", "foot": "<tr>...</tr>", "head": null, "count": 50, "total": 230, "next": "50" }

foot は、合計が要求された場合の合計行で、それ以外は null です。

total と next は null になることがあります。ランタイムは、ページング方式の grid では行を置き換え、無限スクロールでは行を追加します。

グループ化 grid と tree grid(<table role="treegrid">)は、枝を読み込むために POST /_zeyn/grid で path を送ります。これは、開こうとしている行のグループ値(または祖先キー)で、その行の data-zeyn-path にあるものです。レスポンスの形は同じで、rows にはグループ行または 1 階層深い行が入り、total はそれらの数です。ランタイムはそれらを開いた行の後ろに置き、行が閉じるときには、その後ろに続くより深い行をすべて削除します。grid のグループ階層(tree では 32)より長い path は 404 です。

4.2.1 More grid endpoints

エンドポイントBody動作
POST /_zeyn/grid/view{screen, params, node, view}ユーザーのビューを保存します(null で削除)。204。ビューストアがない場合は 404
POST /_zeyn/grid/run{screen, params, node, key, item}行の項目(a0 は行アクション、m2.1 はメニューパス)を実行します。サーバーがキーで行を読み込み、入力を組み立てます。{effects}
POST /_zeyn/grid/bulk{screen, params, node, action, keys, all, query}選択されたキー、または all の場合は query.filters に一致するすべての行に対して、一括 action を実行します。{effects, count}
POST /_zeyn/grid/edit{screen, params, node, key, values, view}インライン編集を保存します。values には、form が送るのと同じ形で、編集した列と行バージョンが入ります。{row}。編集できない列は 403、ほかに 409、422
GET /_zeyn/grid/export?screen&params&node&q={sort,filters,columns}一致するすべての行を CSV としてストリーミングします
POST /_zeyn/menu{screen, params, item}Screen のコンテキストメニューにある action 項目を実行します

4.2.2 Virtual scrolling

data-zeyn-paging="virtual" を持つ grid は、行がいくつあっても、すべての行にわたる 1 つのスクロール可能なリストを表示します。サーバーは最初のブロック(data-zeyn-page-size 行、オフセット 0)を data-zeyn-total 付きでレンダリングします。最初の 64 行はテーブルに、残りはその後ろの <template data-zeyn-rows-later> に入り、grid が 40 列以上を表示する場合は、すべての行がテンプレートに入ります。grid ルートには data-zeyn-row-height(ピクセル)と CSS 変数 --z-row-height もあります。

ランタイムの動作:

  • テーブル本体を、内側のスペーサーの高さが total * rowHeight ピクセル(上限 8,000,000 px)のスクロールコンテナに入れます。上限を超える場合、スクロール位置は比率で行インデックスに対応付けられる(row = scrollTop / (spacerHeight - viewport) * (total - visibleRows))ため、1,000,000 行でも末尾に到達できます。
  • DOM には、表示中の行とその上下 20 行だけを保持し、transform: translateY(...) で配置します。行の高さは固定で、長いテキストは省略記号で切り詰められます。
  • POST /_zeyn/grid で、data-zeyn-page-size 行のブロックを page: { "type": "offset", "offset": blockStart, "limit": blockSize } として読み込み、最初の呼び出しの後は with_total: false にします。最大 20 ブロックかつ最大 100,000 セルをキャッシュに保持し(最も長く使われていないものから破棄。63 列の grid では 7 ブロックで、3 を下回ることはありません)、reload のために保持するブロックも同じ上限に数えられ、最初に破棄され、スクロール方向の次のブロックを先読みし、表示範囲から外れたブロックのリクエストは取り消します(AbortController)。
  • ブロックの読み込み中、表示中の行を消すことはありません。足りない行は、そのブロックが届くまで aria-busy="true" のプレースホルダー行を表示するため、画面がちらつきません。
  • ソートやフィルターが変わると、キャッシュを破棄して先頭へスクロールします。
  • キーボード: 矢印キー、Page Up、Page Down、Home、End でアクティブな行が移動し、表示範囲にスクロールされます。テーブルには aria-rowcount = total が、各行には aria-rowindex が付きます。
  • 位置を示すテキスト(data-zeyn-page-info)は data-zeyn-text-range-total を使います。

4.3 Form submit

POST /_zeyn/form

{ "screen": "product_form", "params": { "id": 7 }, "node": 0, "values": { "code": "P-001", "price": "1.234,56", "active": true } }

値は、ユーザーが入力したままの形で送られます。input と select は文字列、チェックボックスは真偽値です。サーバーはフィールドの型に従ってそれらを解析します。

サーバーは Screen が読み込んだモデルから始め、form 上にあり読み取り専用でないフィールドだけを上書きします。クライアントからのそれ以外の値は無視されます。キーがクライアントから来ることはありません。

レスポンス:

  • 200 { "effects": [ ... ] }: 保存済み。effects を順に実行します。
  • 422 { "errors": [ { "field": "code", "rule": { "rule": "required" }, "message": "Code is required." } ] }: 各メッセージを対応するフィールドの下(#{scope}-{field}-error)に表示し、入力に aria-invalid="true" を設定し、最初の無効な入力にフォーカスします。field が空のエラーは form の上部に表示されます。

4.4 Errors

どのエンドポイントも次のように応答することがあります。

ステータスBody意味
403{ "error": "forbidden", "message": "..." }権限がない、ガードに拒否された、または CSRF トークンが不正
404{ "error": "not_found", "message": "..." }不明な Screen またはノード
409{ "error": "conflict", "message": "..." }ほかの誰かがレコードを変更した
4224.3 を参照検証
504{ "error": "timeout", "message": "..." }呼び出しがサーバーの制限より長くかかった
500{ "error": "internal", "message": "An unexpected error occurred." }詳細はサーバーログにのみ残ります

ランタイムは message をエラートーストとして表示します。

4.5 Language

POST /_zeyn/locale に { "locale": "tr" } を送ると、選択が zeyn_locale Cookie に保存され、204 が返ります。ランタイムはページを再読み込みします。Cookie がない場合、サーバーは Accept-Language から選び、次にアプリのデフォルトを使います。<html lang> は常に選択されたロケールと一致します。

アプリに複数のロケールがある場合、シェルは言語セレクター(select[data-zeyn-locale])を描画します。

4.6 Theme

アプリがセレクターを隠している場合や、テーマが 1 つしかない場合を除き、シェルはテーマセレクター(select[data-zeyn-theme-picker])を描画します。各 option には data-scheme(light、dark、System は空)が付き、アプリのテーマには data-custom が付きます。変更時、ランタイムは <html> の data-theme と data-zeyn-theme、および color-scheme meta を即座に設定し、その後 { "theme": "dark" } を付けて POST /_zeyn/theme を送ります。サーバーは zeyn_theme Cookie を付けて 204 を返し、アプリが提供していないテーマには 404 を返します。ページはその Cookie のテーマでレンダリングされるため、再読み込みもちらつきもありません。docs/theming.md を参照してください。

4.7 Enterprise endpoints

enterprise.js と enterprise.css はランタイムと並べて読み込まれます。詳細は docs/enterprise.md にあります。

エンドポイント目的
POST /_zeyn/reveal {screen, params, node, field, key?}マスクされたフィールドの生の値 {value}。unmask 権限が必要で、常に監査されます
POST /_zeyn/reason {screen, reason}reason が指定された Screen の理由。HttpOnly Cookie 付きの 204
POST /_zeyn/import {screen, params, node, csv, commit}CSV のプレビュー(commit: false)またはインポート
POST /_zeyn/keepaliveアイドル警告後の「サインインしたままにする」。204
GET /_zeyn/health、GET /_zeyn/readyライブネスとレディネス。health はレート制限されず、readiness は probe_ips からのみ
GET /_zeyn/jobsjobs 権限を持つユーザー向けのジョブステータス

すべてのレスポンスに x-zeyn-build が付きます。ランタイムが処理する拒否は、428 reason_required(理由を尋ねてリクエストを繰り返す)と 401 reauth_required(ユーザーに再サインインを求めてリクエストを繰り返す)です。

5. Security

  • すべてのレスポンスは Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self'; style-src-attr 'unsafe-inline'; img-src 'self' data: blob:; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'、X-Content-Type-Options: nosniff、Referrer-Policy: same-origin を設定します。App::csp は、指定した https ホストを img-src、media-src(および blob:)、frame-src、connect-src、font-src に追加します。ほかのディレクティブは変わりません。ヘッダーの文字列は起動時に一度だけ作られます。
  • インラインスクリプトやインラインイベントハンドラーはありません。インラインの style 属性は CSS カスタムプロパティ(--z-width、--z-flex)と固定の列幅にのみ使われるため、ポリシーに style-src-attr 'unsafe-inline' が加わっています。スクリプトは厳格なままです。
  • CSRF トークンは HttpOnly、SameSite=Strict の Cookie に保存され、x-zeyn-csrf ヘッダーと定数時間で比較されます。
  • ページ、totp フォーム、GET /_zeyn/csrf は、トークンを Cookie にあるそのままの形では載せません。レスポンスごとに新しいランダムなパッドの後ろに、トークンとパッドの XOR(16 進数 128 桁)を付けるため、gzip でトークンが漏れることはありません(BREACH)。サーバーは比較の前にマスクを外し、通常の 64 桁のトークンも引き続き受け付けます。
  • 1 KB 以上の動的レスポンス(ページ、部分的な Screen、grid ブロック、エクスポート)は、ブラウザが受け付ける場合にレベル 2 で gzip 圧縮され、Vary: Accept-Encoding が付きます。圧縮しないもの: すでに Content-Encoding があるレスポンス(一度だけ圧縮してキャッシュするフレームワークのアセット)、範囲(Content-Range、および Range を持つすべてのリクエスト)、動画、音声、SVG 以外の画像、application/octet-stream、アーカイブと PDF、text/event-stream、application/x-ndjson(アシスタントのストリーム)、WebSocket のアップグレード。
  • 各呼び出しは次の順に確認します。認証、Screen のアクセス、feature のガード、ノードの action のアクセス。その後、インターセプターと action を実行します。

6. Runtime behavior

  • JavaScript がなくても、ページはすべての grid の最初のページ付きでレンダリングされます。form とページングにはランタイムが必要です。
  • キーボード: 矢印キーで grid の行間を移動し、Enter で行を開き、Escape で最前面のダイアログを閉じ、aria-keyshortcuts のショートカットはテキスト入力の中を除くどこでも動作します。grid では次のとおりです。Space で行を選択、F2 で編集、Ctrl+Z で最後の編集を元に戻す、Shift+F10 またはコンテキストメニューキーで行またはヘッダーのメニューを開く、Shift+クリックで複数列によるソート、Right と Left(右から左のページでは反転)で行の詳細、グループ、子を開閉し、子の行での Left は親へ移動します。行での Right はそのセルの中へ移動します。矢印、Home、End、Ctrl+Home、Ctrl+End、Tab、Shift+Tab でセルカーソルを動かし、Shift で範囲を広げ(セルに aria-selected)、Ctrl+C で範囲をタブ区切りテキストとしてコピーし、Ctrl+V で編集可能なセルへ貼り付け(変更された行ごとに 1 回の POST /_zeyn/grid/edit)ます。40 列以上の grid は、表示範囲内の列だけをページ内に保持します(固定幅の colgroup、colspan を持つ td.z-col-gap のスペーサー、セルの aria-colindex)。
  • 右クリックは、定義されている場所(ヘッダー、data-zeyn-menu を持つ行、data-zeyn-menu を持つ Screen)では Zeyn のメニューを開きます。それ以外の場所ではブラウザ自身のメニューがそのまま使われます。
  • ダイアログが開くとフォーカスがその中へ移動し、閉じると開いた要素に戻ります。
  • クライアント側のチェック(required、minlength、maxlength、min、max、type="email")は、送信前にブラウザの制約検証 API で実行されます。サーバーは常に再度検証します。

7. Component library

zeyn::ui のコンポーネントは、ランタイムの後に head でリンクされる 2 つの追加アセット /_zeyn/components.js と /_zeyn/components.css によって配信されます。ランタイムと同様に、サインインは不要です。

7.1 Endpoints

すべて、JSON と CSRF ヘッダーを使う POST で、最初に Screen のアクセスと feature のガードを確認します。

エンドポイントBody応答
/_zeyn/node{ screen, params, node }1 つのノードの HTML(その grid の最初のページ付き)。遅延 tabs で使われます。
/_zeyn/data{ screen, params, node, key, input }ノードのデータソース key からの JSON。lookup の選択肢(key はフィールド名)、tree の子(key は空)、ステップの確認(key は _step)。ソースの action 権限が確認され、grid と同様にインターセプターが実行されます。action に合わない入力は 404 で拒否されます。
/_zeyn/parse{ kind, text, label }入力された日付または時刻を、サーバーがユーザーの言語、ゾーン、時計でどう解釈するか。{ ok, text, iso, utc, zone } または { ok: false, message }。kind は date、time、datetime、local、presets(今日とクイック範囲)です。

7.2 Values the forms submit

  • Lookup は ID をテキストで送ります。"7"、複数の場合は "3,7,9" です。
  • Lines は "{field}": "rows" と、セルごとに 1 つの値を "{field}.{line}.{column}" として送り、さらに "{field}.{line}._i" も送ります。これは、その行の元になった読み込み済みの行のインデックスです(新しい行では空)。サーバーは各 line を読み込み済みの line から再構築するため、表示されていて編集可能な列だけが変わります。
  • line の検証エラーは { "field": "lines", "rule": { "rule": "line", "value": { "row": 0, "field": "quantity" } } } の形を使います。

7.3 Events

イベント対象Detail
zeyn:contentdocument新しいコンテンツを持つ要素(ナビゲーション、遅延パネル)。両方のスクリプトが、その中身をセットアップします。
zeyn:errorsformランタイムが表示した後の 422 エラー。line エディターとステップ形式の form が使います。
zeyn:effectdocumentランタイムが知らない effect: { effect, source, run }。run(next) は別の effect を実行します。confirm で使われます。

Effect {"effect":"confirm","message":"...","title":"...","ok":"...","cancel":"...","then":{...}} は、モーダルのアラートダイアログで確認し、ユーザーが承諾した後にのみ then を実行します。

7.4 Media

Video と Audio は /_zeyn/media.js と /_zeyn/media.css で配信され、コンポーネントライブラリの後にリンクされます。docs/media.md を参照してください。

エンドポイント動作
GET /_zeyn/media/{source}/{id}登録された media ソースのファイルを range リクエストで返します。200、206、304、416。
GET /_zeyn/media/position?source=&id=サインインしているユーザーが止めた位置 { at, kept }。ソースが位置を保持しない場合、kept は false です(その場合、プレーヤーは local storage を使います)。
POST /_zeyn/media/positionCSRF ヘッダー付きの { source, id, at } で保存します。204。位置が不正なら 422。

7.5 Islands

islands feature を有効にすると、div[data-zeyn-island] を含む Screen は、<meta name="zeyn-islands">(island 名から { js, wasm } の URL への対応)、/_zeyn/islands.js、および 'wasm-unsafe-eval' を含むポリシーとともに配信されます。ほかのページは変わりません。island を含む partial の応答には x-zeyn-islands: 1 が付き、ドキュメントに island のポリシーがない場合、nav.js はそれを完全な読み込みで開きます。

属性またはエンドポイント意味
data-zeyn-island、data-zeyn-load、data-zeyn-propsisland 名、visible、interaction、idle のいずれか、props の JSON
data-zeyn-island-fallbackフォールバックのコンテンツ。island がマウントされると隠れます
data-zeyn-island-stateローダーが設定します: loading、ready、failed
GET /_zeyn/islands/{file}マニフェストに載っているビルド済みファイル。1 年間キャッシュされます
key = リモート名の POST /_zeyn/dataisland からの型付き呼び出し。7.1 のすべての確認が行われます

docs/islands.md を参照してください。

7.6 Realtime

realtime feature を有効にすると、div[data-zeyn-live="topic"] は、サーバーがその topic を通知したときに更新されるノードを示します。これを含むページは /_zeyn/live.js をリンクします。これを含む partial の応答には x-zeyn-live: 1 が付き、nav.js がそのスクリプトをインポートします。要素上の zeyn:reload は、その内部の grid をその場で再読み込みします。

エンドポイント動作
GET /_zeyn/live/topics?t=a,b&csrf=WebSocket。テキストフレーム { "topic": "a" }
GET /_zeyn/live/doc/{kind}/{doc}?csrf=WebSocket。双方向でバイナリフレーム 0x01 + Y update (v1)

ハンドシェイクは、アップグレードの前に、origin、Cookie に対する CSRF トークン、サインインしているユーザー、権限を確認します。docs/realtime.md を参照してください。

プレーヤーは、プログレッシブファイルにフォールバックする前に、そのルートでキャンセル可能な zeyn:media-adaptive イベント({ media, kind, url, hls, dash })を発行します。ストリーミングライブラリは preventDefault() を呼んで処理を引き継ぎます。

8. Desktop class shell

/_zeyn/pro.js と /_zeyn/pro.css は、コンポーネントライブラリの後に読み込まれます。ランタイムと同様に、サインインは不要です。

8.1 Commands and the palette

コマンドは Rust で宣言され(Command::open、Command::navigate、Command::effect、Command::action)、Feature::command で登録されます。シェルは、ユーザーが実行できる(feature のガードを通り、コマンドの access が許可している)コマンドごとに、div[data-zeyn-commands] の中に非表示のボタンを 1 つ描画します。

属性意味
data-zeyn-commandコマンド ID
data-zeyn-effecteffect コマンドの effect。クリック時にランタイムが実行します
data-zeyn-command-runaction コマンドの ID。POST /_zeyn/command で実行されます
aria-keyshortcuts正規化されたショートカット(Control+Alt+N)。ランタイムのショートカット処理がボタンをクリックします
data-hint、data-iconユーザーに表示されるショートカット、アイコン名
コンテナ上の data-text-*ローカライズされたパレットのテキスト

App::check は、次のものを拒否します。読み取れないショートカット、1 文字だけのショートカット(WCAG 2.1.4)、パレット自身のショートカット、2 つのコマンドで使われているショートカットまたは ID、ブラウザがページに渡さないショートカット(BROWSER_RESERVED: Ctrl+N、Ctrl+T、Ctrl+W、Ctrl+Tab、Alt+F4 とその macOS 形式)、そして、コマンドが .override_browser() を呼ばない限り、ブラウザが使うショートカット(BROWSER_DEFAULTS: 印刷、保存、検索、アドレスバー、再読み込み、ズーム、開発者ツール、コピーと貼り付け、F1 から F12)。ツールバーのボタンも実行時に同じルールに従います。そのようなショートカットはバインドされず、理由がログに記録されます。空いている組み合わせは Alt+Shift+<letter> と Ctrl+Alt+<letter> です。

Alt+Shift+P(macOS では Option+Shift+P)がデフォルトでパレットを開きます。App::palette_shortcut("Ctrl+Shift+P") で変更でき、意図的にブラウザが使うキーにすることもできます。ショートカットは data-palette-keys に入り、文字は物理キーで照合されるため、Option や AltGr で壊れません。パレットはメニュー内の button[data-zeyn-palette-open] からも開きます。これは ARIA の combobox と listbox を持つモーダルの <dialog> です。アプリのコマンド、現在の Screen(Window が開いている場合は最前面の Window)のツールバーボタン、メニューバーのコマンド、コンテキストメニューの項目、およびメニューにある Screen を一覧にします。照合はあいまい一致で、大文字小文字とアクセントを無視します。直近に実行した 8 つのコマンドが先頭に来ます(localStorage のキー zeyn.palette.recent)。Escape で閉じ、フォーカスは元の場所に戻ります。

エンドポイントBody応答
POST /_zeyn/command{ command }サーバー側で入力が固定された action コマンドを実行します。CSRF、feature のガード、action の権限、インターセプター。{ effects }。不明な ID と effect コマンドは 404
POST /_zeyn/search{ query }ユーザーが実行できるすべての Feature::search を実行します(CSRF、feature のガード、action の権限、インターセプター)。クエリは 100 文字で切り詰められます。{ hits: [{ label, group, url }] }。最大 20 件で、アプリのパスのみ

コンテナに data-search がある場合、パレットは最後のキー入力の 150 ms 後(2 文字以上)にクエリを送り、コマンドの下にヒットを一覧表示します。最新のクエリへの応答だけが描画されます。ヒットを選ぶと、url の前にベースパスを付けた zeyn:navigate が送られます。その中の #anchor は、Screen が差し替えられた後にその要素へスクロールします。

ランタイムは document 上の zeyn:run を待ち受けるため、ほかのスクリプトはそれを通して処理を実行できます。{ effects, source } は effects を実行し、{ screen, item } は Screen のコンテキストメニュー項目を実行し、{ error } はエラートーストを表示します。

8.2 Menu bar, toolbox, property inspector, status bar

マークアップ動作
ul[role=menubar][data-zeyn-menubar]W3C APG の menubar。トップ項目は、ローヴィングタブストップを持つ button[role=menuitem][aria-haspopup=menu] で、それぞれが閉じている間は hidden の ul[role=menu] を制御します。項目には 8.1 のコマンド属性が付きます。Left と Right でメニュー間を移動し、Down、Enter、Space で開き、Up で最後の項目を開き、Home と End でジャンプし、文字キーでその文字で始まる次の項目へ移動し、Escape で閉じてバーに戻り、Tab と外側のクリックで閉じます。ユーザーが実行できないメニューや項目は描画されません。
div[role=toolbar][aria-orientation=vertical][data-zeyn-toolbox]button.z-tool[aria-pressed] のトグルグループ。矢印、Home、End でローヴィングタブストップが移動します。クリック、Enter、Space でツールを 1 つオンにし、zeyn:tool(detail.tool)を送り、ステータスバーに data-text-selected を書き込みます。
section.z-inspectorForm::section のグループ(role=group の本体を持つ details.z-form-section)を持つ form で、inspector のノードパスの POST /_zeyn/form で保存されます。失敗したフィールドを含むセクションは、ブラウザのチェックの前、および 422 の後に開きます。
div[data-zeyn-statusbar]先頭と末尾の項目、および [data-zeyn-status-live](role=status、aria-live=polite)。Screen 内のどの要素からでも zeyn:status(detail.text)を送ると、ライブテキストが置き換わります。

8.3 Docking panels

div.z-dock[data-zeyn-dock] は、4 つの section.z-dock-area[data-area](left、center、bottom、right)と、3 つの div.z-dock-bar[role=separator][data-edge] の区切りからなる grid です。サイズはカスタムプロパティ --z-dock-left、--z-dock-right(幅に対するパーセント)、--z-dock-bottom(高さに対するパーセント)で、10 から 60 です。各エリアには、ドラッグ可能な button.z-dock-tab[data-panel] の role=tablist、オプションボタン、子ノードを収める role=tabpanel のパネルがあります(パスはどのコンテナとも同じで、パネルが移動しても変わりません)。開いているパネルがない側面のエリアは隠れます。

キーボード: Left と Right(右から左のページでは反転)、Home、End でエリアの開いているタブ間を移動します。Delete でフォーカスされたパネルを閉じます。Shift+F10 またはコンテキストメニューキーでエリアのオプション(別のエリアへ移動、閉じる、閉じたパネルを表示)を開きます。区切りの上の矢印キーは 5 パーセントずつサイズを変更し、Home と End は限界へ移動します。MenuList::panels で作ったメニューバーのエントリは、すべてのパネルを menuitemcheckbox として一覧表示します。変更は [data-zeyn-dock-live] で通知されます。

エンドポイントBody応答
POST /_zeyn/dock{ screen, params, node, save: false }{ layout }: サインインしているユーザーの保存済みレイアウト、または null
POST /_zeyn/dock{ screen, params, node, save: true, layout }204。layout: null で削除します

layout は、タブ順に並んだ { sizes: { left, right, bottom }, panels: [{ id, area, open, active }] } です。サーバーは CSRF、Screen のアクセス、feature のガードを確認し、dock 自身のパネルだけを(それぞれ一度ずつ)残してサイズを範囲内に収め、アプリのビューストア(App::datagrid_views)に、テナント、Screen、ノードをキーとして保存します。ストアがない場合、エンドポイントは 404 を、ユーザーがいない場合は 403 を返し、その場合ブラウザはレイアウトを localStorage に保持します。

9. AI agents: MCP and WebMCP

外部の AI エージェントは、2 つの入口を通してアプリにアクセスします。どちらもデフォルトでは無効で、App::mcp で有効にします。

設定MCP エンドポイントWebMCP ツール書き込み
Mcp::off()(デフォルト)404404なし
Mcp::read_only()有効有効なし
Mcp::on()有効有効form の保存と、書き込みを行う API action

.server(false) と .webmcp(false) で、片方の入口を無効にできます。zeyn new で作られる新しいプロジェクトは Mcp::read_only() で始まります。

無効がデフォルトの理由: このエンドポイントでは、有効な Authorization ヘッダーを持つ誰もが、モデルを通してアプリを操作できてしまい、モデルは読み込んだデータによって誘導されうるからです。フレームワークはすべてのアプリをエージェント対応にします(ツールはコードから生成され、書くものはありません)が、アプリがその入口を開くかどうか、そしてエージェントに書き込みを許すかどうかは、運用する人々が決めることです。読み取り専用は 1 行で有効にできます。

9.1 Tools

ツールは、サインインしているユーザー向けにアプリから作られます。

  • grid ごとに 1 つの検索ツール({screen}_{node}_search、入力: DataQuery)。書き込みが有効な場合は、params なしで開く、ルーティングされたすべての Screen の form ごとに 1 つの保存ツール({screen}_{node}_save、入力: 入力されたままの form の値)。
  • 公開された API action ごとに 1 つのツール(api_{feature}_{action})。入力が DataQuery の action は読み取りで、それ以外の API action はすべて書き込みとして扱われます。

説明は、Screen のタイトル、Screen と action の /// ドキュメントコメントの最初の段落、およびそれらの ai = "..." ガイダンスから作られます。ユーザーが使えない grid と form、および ai = false が付いた Screen と action は、一覧に出ることはなく、呼び出されても拒否されます。

#[action(confirm)] は、ユーザーがアプリ内で承諾した後にのみモデルが実行できる action を示します。/_zeyn/mcp はそのようなツールを一覧にも実行もしません(そのチャネルには承諾できる人がいないため)。WebMCP ツールはモーダルダイアログ(zeyn:confirm)で確認し、その後 form と一緒に confirmed: true を送ります。組み込みのアシスタントはチャットで確認します(docs/assistant.md を参照)。

9.2 Endpoints

エンドポイント動作
GET /_zeyn/tools?screen&params表示中の Screen 向けの { tools: [{ name, description, inputSchema, kind, screen, node, writes, confirm }], texts }。zeyn.js がそれらを document.modelContext に登録します
POST /_zeyn/mcpJSON-RPC: initialize、tools/list、tools/call。Authorization ヘッダーとサインインしているユーザーが必要です
via_agent: true 付きの POST /_zeyn/formWebMCP の form 保存。WebMCP 付きの Mcp::on() が必要です。confirmed: true はユーザーが承諾した後にのみ

エージェントの呼び出しはすべてユーザーとして実行されます。Screen のアクセス、ガード、action の権限、インターセプター、レート制限、時間制限が適用されます。エージェントの呼び出しは常に監査され(via_agent: true)、マスクされたフィールドは、ユーザーが表示できる場合でも、エージェントに対しては常にマスクされたままです。OpenAPI は各操作に x-zeyn-ai を付けます。ai = false の場合は false、それ以外は { hint, confirm, writes } です。