{
  "uuid": "",
  "components": [
    {
      "type": "content",
      "key": "styleBlock",
      "label": "Custom CSS (styleBlock)",
      "input": false,
      "hideLabel": true,
      "tableView": false,
      "persistent": false,
      "content": ".formio-form { font-family: 'Segoe UI', Meiryo, sans-serif; } .ks-header { background: linear-gradient(135deg, #1e88e5 0%, #1565c0 100%); color: white; padding: 20px; border-radius: 8px 8px 0 0; margin-bottom: 20px; } .ks-header h2 { margin: 0; font-size: 24px; } .section-title { background: #f5f5f5; padding: 12px 16px; border-left: 4px solid #1e88e5; margin: 20px 0 15px 0; font-weight: bold; color: #333; font-size: 16px; } .ks-note { background: #e3f2fd; border-left: 4px solid #1e88e5; padding: 10px 14px; border-radius: 4px; font-size: 12px; color: #333; margin-bottom: 15px; line-height: 1.7; } .ks-note code { background: #fff; padding: 1px 5px; border-radius: 3px; color: #c2185b; } .ks-cfglobal.formio-component-panel > .card { background: #ede7f6; border: 1px solid #b39ddb; } .formio-component-editgrid .list-group-item { border: 1px solid #e0e0e0; margin-bottom: 8px; border-radius: 6px; } .lc-segmented .form-radio { display: flex; gap: 6px; flex-wrap: wrap; } .lc-segmented .form-check { padding-left: 0; margin: 0 4px 4px 0; } .lc-segmented .form-check-input { position: absolute; opacity: 0; width: 1px; height: 1px; pointer-events: none; } .lc-segmented .form-check-label { display: block; padding: 6px 16px; border: 1px solid #90a4ae; border-radius: 16px; background: #fff; color: #455a64; cursor: pointer; font-size: 12px; } .lc-segmented .form-check-input:checked + span { color: inherit; } .lc-segmented .form-check-input:checked + .form-check-label, .lc-segmented .form-check-label:has(.form-check-input:checked) { background: #1e88e5; color: #fff; border-color: #1565c0; font-weight: bold; } .lc-segmented .form-check-input:focus + .form-check-label, .lc-segmented .form-check-label:has(.form-check-input:focus) { box-shadow: 0 0 0 2px rgba(30,136,229,.35); } .lc-picker ~ .formio-dialog-buttons { display: none !important; } .chart-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 20px; padding: 20px; } .chart-card { background: #fff; border: 1px solid #e0e0e0; border-radius: 8px; padding: 15px; } .chart-card h4 { margin: 0 0 15px 0; font-size: 14px; color: #333; border-bottom: 2px solid #1e88e5; padding-bottom: 8px; } .chart-wrapper { position: relative; height: 240px; } .formio-component-mainTabs { margin-bottom: 10px; } .ks-chartpanel { margin-top: 40px; } .ks-chartpanel.formio-component-panel > .card { border: 2px solid #90caf9; border-radius: 8px; } .ks-chartpanel.formio-component-panel > .card > .card-header { background: #e3f2fd; } .ks-block { margin-bottom: 22px; } .ks-block.formio-component-panel > .card { border: 1px solid #cfd8dc; border-left: 4px solid #1e88e5; border-radius: 6px; box-shadow: 0 1px 3px rgba(0,0,0,.07); } .ks-block.formio-component-panel > .card > .card-header { background: linear-gradient(180deg, #f8fafc, #eef3f7); padding: 10px 16px; border-bottom: 1px solid #dde5ec; } .ks-block.formio-component-panel > .card > .card-header .card-title { font-size: 14px; font-weight: bold; color: #1565c0; } .ks-block.formio-component-panel > .card > .card-body { padding: 16px 16px 8px 16px; background: #fff; } .formio-component-mainTabs > .card { border: 1px solid #cfd8dc; border-radius: 8px; } .formio-component-mainTabs .nav-tabs .nav-link.active { font-weight: bold; border-top: 3px solid #1e88e5; } .formio-dialog:has(.ks-docpicker) .formio-dialog-content { width: 92% !important; max-width: 1100px; max-height: 90vh !important; overflow: auto !important; } .ks-docpicker > .card, .ks-docpicker > .mb-2.card { border: none !important; box-shadow: none !important; margin: 0 !important; background: transparent !important; } .ks-docpicker > .card > .card-header { background: #fff !important; border: none !important; border-bottom: 2px solid #1e88e5 !important; padding: 10px 4px !important; } .ks-docpicker > .card > .card-header .card-title { font-size: 15px; font-weight: bold; color: #1565c0; } .ks-docpicker > .card > .card-body { padding: 14px 4px !important; background: #fff !important; } .ks-docpicker-cell .open-modal-button { background: #1e88e5; color: #fff; border: 1px solid #1565c0; border-radius: 4px; padding: 6px 16px; margin-top: 24px; } .ks-docpicker-cell [ref=\"modalLabelValue\"], .ks-docpicker-cell [ref=\"modalPreviewLiveRegion\"] { display: none !important; } .ks-config-field { background: #fff8e1; border: 2px solid #ffb300; border-radius: 6px; padding: 10px 12px 2px 12px; } .ks-config-field label { color: #e65100 !important; font-weight: bold !important; } .ks-config-field label::before { content: '\\2605 '; color: #ff8f00; } .ks-config-field input { background: #fffdf5; border-color: #ffcc80; } .ks-url { background: #263238; color: #aed581; font-family: Consolas, \"Courier New\", monospace; font-size: 11px; padding: 8px 12px; border-radius: 4px; word-break: break-all; margin-bottom: 12px; line-height: 1.5; user-select: text; } .ks-url .ks-url-tag { color: #80cbc4; font-weight: bold; margin-right: 8px; }\n/* ks-spec */\n.ks-spec { width: 100%; border-collapse: collapse; font-size: 12px; margin-bottom: 16px; }\n.ks-spec th, .ks-spec td { border: 1px solid #cfd8dc; padding: 7px 10px; vertical-align: top; line-height: 1.6; }\n.ks-spec th { background: #eef3f7; color: #1565c0; text-align: left; }\n.ks-spec td:first-child { background: #fafbfc; font-weight: bold; white-space: nowrap; }\n.ks-spec code { background: #fff3e0; padding: 1px 5px; border-radius: 3px; color: #c2185b; }\n.ks-run-btn { margin-top: 28px; }\n\n/* ks-parts */\n.ks-howto { background: #e8f5e9; border-left: 4px solid #43a047; padding: 10px 14px; border-radius: 4px; font-size: 12px; color: #1b5e20; margin: 6px 0 14px 0; line-height: 1.7; }\n.ks-howto code { background: #fff; padding: 1px 5px; border-radius: 3px; color: #2e7d32; }\n.ks-part.formio-component-panel > .card { border: 1px dashed #90a4ae; border-radius: 6px; margin-bottom: 16px; }\n.ks-part.formio-component-panel > .card > .card-header { background: #f5f7f9; padding: 8px 14px; }\n.ks-part.formio-component-panel > .card > .card-header .card-title { font-size: 13px; font-weight: bold; color: #37474f; }\n"
    },
    {
      "type": "content",
      "key": "styleBlockGlobal",
      "label": "Global CSS (styleBlockGlobal)",
      "input": false,
      "hideLabel": true,
      "tableView": false,
      "persistent": false,
      "content": "/* styleBlockGlobal デモ: このブロックはスコープ化されず head に直接注入されます。フォーム外の XPages パネルを .cf-panel-<キー> で狙えます。例) .cf-panel-childDocsHier { display: none; }  以下は副作用のない無害なデモルールです。 */ .cf-panel-webForm { scroll-margin-top: 4px; }"
    },
    {
      "type": "htmlelement",
      "tag": "div",
      "className": "ks-header",
      "key": "header",
      "content": "<h2>🧰 consentFlow キッチンシンク</h2><p style='margin: 10px 0 0 0; opacity: 0.9;'>Form.io 標準コンポーネント + consentFlow 固有機能 (cfGlobal / cfAPI / restViewJson / styleBlock / Chart.js) の実装リファレンス</p>",
      "input": false,
      "tableView": false
    },
    {
      "type": "htmlelement",
      "tag": "div",
      "className": "ks-note",
      "key": "aboutNote",
      "content": "このフォームは <strong>開発者向けリファレンス</strong> です。各タブの先頭に実装ポイントの解説があります。<br>・レイアウトは <code>panel</code>/<code>columns</code>/<code>tabs</code> を使用 — <code>container</code> はデータがネストし Notes のフラット保存 (<code>&lt;接頭語&gt;&lt;key&gt;</code>) と不整合のため不使用<br>・<code>signature</code>/<code>submit</code> は Notes アイテム同期対象外、<code>file</code> 型はクラウドストレージ経路のため本サンプルでは標準の添付ファイルパネルを利用",
      "input": false,
      "tableView": false
    },
    {
      "type": "button",
      "key": "btnStyleToggle",
      "label": "🎨 プレーン表示に切替",
      "action": "custom",
      "theme": "secondary",
      "size": "sm",
      "input": true,
      "tableView": false,
      "showValidations": false,
      "custom": "// コアの styleBlock 注入 <style> (scoped/global 両方) を disabled で一括切替\nvar styles = document.querySelectorAll('style[data-cf-styleblock]');\nif (!styles.length) { alert('styleBlock の注入が見つかりません — この画面ではスタイル切替できません'); return; }\nwindow._cfPlainMode = !window._cfPlainMode;\nfor (var i = 0; i < styles.length; i++) { styles[i].disabled = window._cfPlainMode; }\n// ボタンの表示を現在の状態に合わせて更新\nvar btnEl = instance.element ? instance.element.querySelector('button') : null;\nif (btnEl) { btnEl.textContent = window._cfPlainMode ? '🎨 スタイルを元に戻す' : '🎨 プレーン表示に切替'; }",
      "description": "styleBlock 由来の装飾だけを一括 ON/OFF して素の Form.io 表示と比較できます (インラインスタイルの装飾は残ります)"
    },
    {
      "type": "panel",
      "key": "cfInfoPanel",
      "title": "🌐 cfGlobal ライブ表示 (REST ゼロ・保存されません ※設定「persistent:false / Client-only の項目を保存しない」が はい のとき)",
      "customClass": "ks-cfglobal",
      "collapsible": true,
      "collapsed": false,
      "input": false,
      "tableView": false,
      "components": [
        {
          "type": "htmlelement",
          "tag": "div",
          "className": "ks-note",
          "key": "cfInfoNote",
          "content": "サーバ値の読出し優先順は <code>cfGlobal 直読 &gt; cfAPI (+isNewNoteガード) &gt; restViewJson/restDocJson</code>。以下はすべて cfGlobal 直読 (<code>calculateValue</code> + <code>persistent:false</code>) です。",
          "input": false,
          "tableView": false
        },
        {
          "type": "columns",
          "key": "cfInfoCols1",
          "label": "Columns",
          "input": false,
          "tableView": false,
          "columns": [
            {
              "components": [
                {
                  "type": "textfield",
                  "key": "cfUserDisp",
                  "label": "ログインユーザー",
                  "input": true,
                  "tableView": false,
                  "disabled": true,
                  "persistent": false,
                  "calculateValue": "if (typeof cfGlobal !== 'undefined') { value = cfGlobal.currentUserKanjiName || cfGlobal.currentUserName || ''; } else { value = '(ビルダープレビュー)'; }",
                  "allowCalculateOverride": false
                }
              ],
              "width": 4,
              "offset": 0,
              "push": 0,
              "pull": 0,
              "size": "md",
              "currentWidth": 4
            },
            {
              "components": [
                {
                  "type": "textfield",
                  "key": "cfSectionDisp",
                  "label": "所属 / 役職",
                  "input": true,
                  "tableView": false,
                  "disabled": true,
                  "persistent": false,
                  "calculateValue": "if (typeof cfGlobal !== 'undefined') { value = (cfGlobal.sectionName || '') + ' ' + (cfGlobal.postName || ''); } else { value = '(ビルダープレビュー)'; }",
                  "allowCalculateOverride": false
                }
              ],
              "width": 4,
              "offset": 0,
              "push": 0,
              "pull": 0,
              "size": "md",
              "currentWidth": 4
            },
            {
              "components": [
                {
                  "type": "textfield",
                  "key": "cfModeDisp",
                  "label": "文書モード",
                  "input": true,
                  "tableView": false,
                  "disabled": true,
                  "persistent": false,
                  "calculateValue": "if (typeof cfGlobal !== 'undefined') { value = cfGlobal.isNewNote ? '新規作成' : '既存文書'; } else { value = '(ビルダープレビュー)'; }",
                  "allowCalculateOverride": false
                }
              ],
              "width": 4,
              "offset": 0,
              "push": 0,
              "pull": 0,
              "size": "md",
              "currentWidth": 4
            }
          ]
        },
        {
          "type": "columns",
          "key": "cfInfoCols2",
          "label": "Columns",
          "input": false,
          "tableView": false,
          "columns": [
            {
              "components": [
                {
                  "type": "textfield",
                  "key": "cfStatusDisp",
                  "label": "StatusNo",
                  "input": true,
                  "tableView": false,
                  "disabled": true,
                  "persistent": false,
                  "calculateValue": "if (typeof cfGlobal !== 'undefined') { value = (cfGlobal.statusNo === '' ? '(未申請)' : cfGlobal.statusNo); } else { value = '(ビルダープレビュー)'; }",
                  "allowCalculateOverride": false
                }
              ],
              "width": 4,
              "offset": 0,
              "push": 0,
              "pull": 0,
              "size": "md",
              "currentWidth": 4
            },
            {
              "components": [
                {
                  "type": "textfield",
                  "key": "cfAppNoDisp",
                  "label": "ApplicationNo",
                  "input": true,
                  "tableView": false,
                  "disabled": true,
                  "persistent": false,
                  "calculateValue": "if (typeof cfGlobal !== 'undefined') { value = (cfGlobal.applicationNo === '' ? '(未採番)' : cfGlobal.applicationNo); } else { value = '(ビルダープレビュー)'; }",
                  "allowCalculateOverride": false
                }
              ],
              "width": 4,
              "offset": 0,
              "push": 0,
              "pull": 0,
              "size": "md",
              "currentWidth": 4
            },
            {
              "components": [
                {
                  "type": "textfield",
                  "key": "cfRevDisp",
                  "label": "言語 / フォームリビジョン",
                  "input": true,
                  "tableView": false,
                  "disabled": true,
                  "persistent": false,
                  "calculateValue": "if (typeof cfGlobal !== 'undefined') { value = (cfGlobal.lang || '') + ' / rev ' + (cfGlobal.webFormRevisionNo || ''); } else { value = '(ビルダープレビュー)'; }",
                  "allowCalculateOverride": false
                }
              ],
              "width": 4,
              "offset": 0,
              "push": 0,
              "pull": 0,
              "size": "md",
              "currentWidth": 4
            }
          ]
        }
      ]
    },
    {
      "type": "tabs",
      "key": "mainTabs",
      "label": "Tabs",
      "input": false,
      "tableView": false,
      "components": [
        {
          "key": "tabBasic",
          "label": "📝 基本入力",
          "components": [
            {
              "type": "htmlelement",
              "tag": "div",
              "className": "ks-note",
              "key": "introBasic",
              "content": "テキスト系コンポーネント。<code>key</code> がそのまま Notes アイテム <code>&lt;接頭語&gt;&lt;key&gt;</code> (既定 <code>CF_&lt;key&gt;</code>) に 1:1 マッピングされます。webform 側に接頭語はハードコードしません。",
              "input": false,
              "tableView": false
            },
            {
              "type": "panel",
              "key": "ksBlockText",
              "title": "✏️ テキスト系入力",
              "customClass": "ks-block",
              "collapsible": true,
              "collapsed": false,
              "input": false,
              "tableView": false,
              "components": [
                {
                  "type": "columns",
                  "key": "nameCols",
                  "label": "Columns",
                  "input": false,
                  "tableView": false,
                  "columns": [
                    {
                      "components": [
                        {
                          "type": "textfield",
                          "key": "lastName",
                          "label": "姓",
                          "input": true,
                          "tableView": false,
                          "placeholder": "例: 山田",
                          "validate": {
                            "required": true
                          }
                        }
                      ],
                      "width": 6,
                      "offset": 0,
                      "push": 0,
                      "pull": 0,
                      "size": "md",
                      "currentWidth": 6
                    },
                    {
                      "components": [
                        {
                          "type": "textfield",
                          "key": "firstName",
                          "label": "名",
                          "input": true,
                          "tableView": false,
                          "placeholder": "例: 太郎"
                        }
                      ],
                      "width": 6,
                      "offset": 0,
                      "push": 0,
                      "pull": 0,
                      "size": "md",
                      "currentWidth": 6
                    }
                  ]
                },
                {
                  "type": "columns",
                  "key": "contactCols",
                  "label": "Columns",
                  "input": false,
                  "tableView": false,
                  "columns": [
                    {
                      "components": [
                        {
                          "type": "email",
                          "key": "emailField",
                          "label": "メールアドレス",
                          "input": true,
                          "tableView": false,
                          "placeholder": "taro@example.com"
                        }
                      ],
                      "width": 6,
                      "offset": 0,
                      "push": 0,
                      "pull": 0,
                      "size": "md",
                      "currentWidth": 6
                    },
                    {
                      "components": [
                        {
                          "type": "phoneNumber",
                          "key": "phoneField",
                          "label": "電話番号",
                          "input": true,
                          "tableView": false,
                          "inputMask": "999-9999-9999"
                        }
                      ],
                      "width": 6,
                      "offset": 0,
                      "push": 0,
                      "pull": 0,
                      "size": "md",
                      "currentWidth": 6
                    }
                  ]
                },
                {
                  "type": "columns",
                  "key": "webCols",
                  "label": "Columns",
                  "input": false,
                  "tableView": false,
                  "columns": [
                    {
                      "components": [
                        {
                          "type": "url",
                          "key": "urlField",
                          "label": "Web サイト (url)",
                          "input": true,
                          "tableView": false,
                          "placeholder": "https://example.com"
                        }
                      ],
                      "width": 6,
                      "offset": 0,
                      "push": 0,
                      "pull": 0,
                      "size": "md",
                      "currentWidth": 6
                    },
                    {
                      "components": [
                        {
                          "type": "password",
                          "key": "passwordField",
                          "label": "パスワード (デモ用・保存されません ※設定「persistent:false / Client-only の項目を保存しない」が はい のとき)",
                          "input": true,
                          "tableView": false,
                          "persistent": false,
                          "protected": true
                        }
                      ],
                      "width": 6,
                      "offset": 0,
                      "push": 0,
                      "pull": 0,
                      "size": "md",
                      "currentWidth": 6
                    }
                  ]
                },
                {
                  "type": "textfield",
                  "key": "limitedText",
                  "label": "文字数制限付きテキスト (5〜20 文字)",
                  "input": true,
                  "tableView": false,
                  "showCharCount": true,
                  "validate": {
                    "minLength": 5,
                    "maxLength": 20
                  },
                  "description": "validate.minLength / maxLength + showCharCount のデモ"
                },
                {
                  "type": "textarea",
                  "key": "memoArea",
                  "label": "テキストエリア (自動拡張)",
                  "rows": 3,
                  "input": true,
                  "tableView": false,
                  "autoExpand": true,
                  "showCharCount": true,
                  "placeholder": "複数行の入力"
                },
                {
                  "type": "tags",
                  "key": "tagsField",
                  "label": "タグ (カンマ区切り文字列で保存)",
                  "input": true,
                  "tableView": false,
                  "storeas": "string",
                  "description": "storeas:\"string\" — Notes 側は 1 テキストアイテムに収まります"
                }
              ]
            },
            {
              "type": "panel",
              "key": "ksBlockNumeric",
              "title": "🔢 数値・通貨・チェックボックス",
              "customClass": "ks-block",
              "collapsible": true,
              "collapsed": false,
              "input": false,
              "tableView": false,
              "components": [
                {
                  "type": "columns",
                  "key": "amountCols",
                  "label": "Columns",
                  "input": false,
                  "tableView": false,
                  "columns": [
                    {
                      "components": [
                        {
                          "type": "number",
                          "key": "numberField",
                          "label": "数値 (0〜100)",
                          "input": true,
                          "tableView": false,
                          "validate": {
                            "min": 0,
                            "max": 100
                          }
                        }
                      ],
                      "width": 6,
                      "offset": 0,
                      "push": 0,
                      "pull": 0,
                      "size": "md",
                      "currentWidth": 6
                    },
                    {
                      "components": [
                        {
                          "type": "currency",
                          "key": "currencyField",
                          "label": "通貨 (JPY)",
                          "currency": "JPY",
                          "input": true,
                          "tableView": false
                        }
                      ],
                      "width": 6,
                      "offset": 0,
                      "push": 0,
                      "pull": 0,
                      "size": "md",
                      "currentWidth": 6
                    }
                  ]
                },
                {
                  "type": "checkbox",
                  "key": "agreeCheck",
                  "label": "内容を確認しました (checkbox)",
                  "input": true,
                  "tableView": false,
                  "defaultValue": false
                }
              ]
            },
            {
              "type": "panel",
              "key": "addressPanel",
              "title": "🏠 住所 (address コンポーネントの代替 — フラット保存)",
              "collapsible": true,
              "input": false,
              "tableView": false,
              "components": [
                {
                  "type": "htmlelement",
                  "tag": "div",
                  "className": "ks-note",
                  "key": "addressNote",
                  "content": "Form.io 標準の <code>address</code> はデータがネストするため、consentFlow では panel + 個別フィールドでフラットに構成します。",
                  "input": false,
                  "tableView": false
                },
                {
                  "type": "columns",
                  "key": "addressCols1",
                  "label": "Columns",
                  "input": false,
                  "tableView": false,
                  "columns": [
                    {
                      "components": [
                        {
                          "type": "textfield",
                          "key": "zipCode",
                          "label": "郵便番号",
                          "input": true,
                          "tableView": false,
                          "inputMask": "999-9999",
                          "placeholder": "460-0000"
                        }
                      ],
                      "width": 4,
                      "offset": 0,
                      "push": 0,
                      "pull": 0,
                      "size": "md",
                      "currentWidth": 4
                    },
                    {
                      "components": [
                        {
                          "type": "textfield",
                          "key": "prefecture",
                          "label": "都道府県",
                          "input": true,
                          "tableView": false,
                          "placeholder": "愛知県"
                        }
                      ],
                      "width": 8,
                      "offset": 0,
                      "push": 0,
                      "pull": 0,
                      "size": "md",
                      "currentWidth": 8
                    }
                  ]
                },
                {
                  "type": "columns",
                  "key": "addressCols2",
                  "label": "Columns",
                  "input": false,
                  "tableView": false,
                  "columns": [
                    {
                      "components": [
                        {
                          "type": "textfield",
                          "key": "cityName",
                          "label": "市区町村",
                          "input": true,
                          "tableView": false,
                          "placeholder": "名古屋市中区"
                        }
                      ],
                      "width": 6,
                      "offset": 0,
                      "push": 0,
                      "pull": 0,
                      "size": "md",
                      "currentWidth": 6
                    },
                    {
                      "components": [
                        {
                          "type": "textfield",
                          "key": "streetAddress",
                          "label": "番地・建物名",
                          "input": true,
                          "tableView": false
                        }
                      ],
                      "width": 6,
                      "offset": 0,
                      "push": 0,
                      "pull": 0,
                      "size": "md",
                      "currentWidth": 6
                    }
                  ]
                }
              ],
              "customClass": "ks-block",
              "collapsed": false
            }
          ]
        },
        {
          "key": "tabSelect",
          "label": "☑️ 選択・日付",
          "components": [
            {
              "type": "htmlelement",
              "tag": "div",
              "className": "ks-note",
              "key": "introSelect",
              "content": "選択系と日付系。multi-select の <code>validate.custom</code> は値が string|array の両方で来るため<strong>配列正規化が必須</strong>。日付の動的初期値は公開時に FieldJson へ焼き込まれるため <code>calculateValue + isNewNote ガード</code>で実装します。",
              "input": false,
              "tableView": false
            },
            {
              "type": "panel",
              "key": "ksBlockChoice",
              "title": "🔘 単一/複数選択 (radio / selectboxes / select)",
              "customClass": "ks-block",
              "collapsible": true,
              "collapsed": false,
              "input": false,
              "tableView": false,
              "components": [
                {
                  "type": "radio",
                  "key": "radioNormal",
                  "label": "オプション (radio)",
                  "input": true,
                  "tableView": false,
                  "inline": true,
                  "optionsLabelPosition": "right",
                  "defaultValue": "",
                  "values": [
                    {
                      "label": "オプション１",
                      "value": "opt1",
                      "shortcut": ""
                    },
                    {
                      "label": "オプション２",
                      "value": "opt2",
                      "shortcut": ""
                    },
                    {
                      "label": "オプション３",
                      "value": "opt3",
                      "shortcut": ""
                    }
                  ]
                },
                {
                  "type": "radio",
                  "key": "satisfaction",
                  "label": "満足度 (radio + lc-segmented ボタン風 CSS)",
                  "input": true,
                  "tableView": false,
                  "inline": true,
                  "customClass": "lc-segmented",
                  "defaultValue": "",
                  "values": [
                    {
                      "label": "1 不満",
                      "value": "1",
                      "shortcut": ""
                    },
                    {
                      "label": "2 やや不満",
                      "value": "2",
                      "shortcut": ""
                    },
                    {
                      "label": "3 普通",
                      "value": "3",
                      "shortcut": ""
                    },
                    {
                      "label": "4 やや満足",
                      "value": "4",
                      "shortcut": ""
                    },
                    {
                      "label": "5 満足",
                      "value": "5",
                      "shortcut": ""
                    }
                  ],
                  "description": "styleBlock の .lc-segmented CSS (:has() 併用で選択状態を表示) のデモ"
                },
                {
                  "type": "selectboxes",
                  "key": "drinkBoxes",
                  "label": "好きな飲み物 (selectboxes・3 つまで)",
                  "input": true,
                  "tableView": false,
                  "inputType": "checkbox",
                  "values": [
                    {
                      "label": "紅茶",
                      "value": "tea",
                      "shortcut": ""
                    },
                    {
                      "label": "コーヒー",
                      "value": "coffee",
                      "shortcut": ""
                    },
                    {
                      "label": "ココア",
                      "value": "cocoa",
                      "shortcut": ""
                    },
                    {
                      "label": "緑茶",
                      "value": "greenTea",
                      "shortcut": ""
                    }
                  ],
                  "validate": {
                    "custom": "var cnt = 0; if (input && typeof input === 'object') { for (var k in input) { if (input[k] === true) cnt++; } } valid = (cnt <= 3) ? true : '3 つまで選択できます';"
                  }
                },
                {
                  "type": "columns",
                  "key": "selectCols",
                  "label": "Columns",
                  "input": false,
                  "tableView": false,
                  "columns": [
                    {
                      "components": [
                        {
                          "type": "select",
                          "key": "citySelect",
                          "label": "市の選択 (select・愛知)",
                          "input": true,
                          "tableView": false,
                          "widget": "choicesjs",
                          "dataSrc": "values",
                          "data": {
                            "values": [
                              {
                                "label": "名古屋市",
                                "value": "nagoya"
                              },
                              {
                                "label": "豊橋市",
                                "value": "toyohashi"
                              },
                              {
                                "label": "岡崎市",
                                "value": "okazaki"
                              },
                              {
                                "label": "一宮市",
                                "value": "ichinomiya"
                              },
                              {
                                "label": "瀬戸市",
                                "value": "seto"
                              },
                              {
                                "label": "半田市",
                                "value": "handa"
                              },
                              {
                                "label": "春日井市",
                                "value": "kasugai"
                              },
                              {
                                "label": "豊川市",
                                "value": "toyokawa"
                              },
                              {
                                "label": "津島市",
                                "value": "tsushima"
                              },
                              {
                                "label": "碧南市",
                                "value": "hekinan"
                              },
                              {
                                "label": "刈谷市",
                                "value": "kariya"
                              },
                              {
                                "label": "西尾市",
                                "value": "nishio"
                              },
                              {
                                "label": "犬山市",
                                "value": "inuyama"
                              }
                            ]
                          },
                          "defaultValue": "",
                          "template": "<span>{{ item.label }}</span>",
                          "searchEnabled": true,
                          "lazyLoad": true
                        }
                      ],
                      "width": 6,
                      "offset": 0,
                      "push": 0,
                      "pull": 0,
                      "size": "md",
                      "currentWidth": 6
                    },
                    {
                      "components": [
                        {
                          "type": "select",
                          "key": "fruitsMulti",
                          "label": "フルーツ (multi-select・2 つまで)",
                          "input": true,
                          "tableView": false,
                          "widget": "choicesjs",
                          "dataSrc": "values",
                          "data": {
                            "values": [
                              {
                                "label": "マンゴー",
                                "value": "mango"
                              },
                              {
                                "label": "りんご",
                                "value": "apple"
                              },
                              {
                                "label": "パイナップル",
                                "value": "pineapple"
                              },
                              {
                                "label": "ぶどう",
                                "value": "grapes"
                              }
                            ]
                          },
                          "defaultValue": [],
                          "template": "<span>{{ item.label }}</span>",
                          "searchEnabled": true,
                          "lazyLoad": true,
                          "multiple": true,
                          "validate": {
                            "custom": "var arr = Array.isArray(input) ? input : (input ? [input] : []); valid = (arr.length <= 2) ? true : 'フルーツは 2 つまでです';"
                          },
                          "description": "validate.custom は string|array 両対応の配列正規化で判定"
                        }
                      ],
                      "width": 6,
                      "offset": 0,
                      "push": 0,
                      "pull": 0,
                      "size": "md",
                      "currentWidth": 6
                    }
                  ]
                }
              ]
            },
            {
              "type": "panel",
              "key": "ksBlockDate",
              "title": "📅 日付・時刻 (day / time / datetime)",
              "customClass": "ks-block",
              "collapsible": true,
              "collapsed": false,
              "input": false,
              "tableView": false,
              "components": [
                {
                  "type": "columns",
                  "key": "dateCols",
                  "label": "Columns",
                  "input": false,
                  "tableView": false,
                  "columns": [
                    {
                      "components": [
                        {
                          "type": "day",
                          "key": "dayField",
                          "label": "年月日 (day)",
                          "input": true,
                          "tableView": false,
                          "dayFirst": false
                        }
                      ],
                      "width": 4,
                      "offset": 0,
                      "push": 0,
                      "pull": 0,
                      "size": "md",
                      "currentWidth": 4
                    },
                    {
                      "components": [
                        {
                          "type": "time",
                          "key": "timeField",
                          "label": "時刻 (time)",
                          "input": true,
                          "tableView": false,
                          "inputType": "time"
                        }
                      ],
                      "width": 4,
                      "offset": 0,
                      "push": 0,
                      "pull": 0,
                      "size": "md",
                      "currentWidth": 4
                    },
                    {
                      "components": [
                        {
                          "type": "datetime",
                          "key": "applicationDate",
                          "label": "申請日 (新規時に自動で今日)",
                          "input": true,
                          "tableView": false,
                          "format": "yyyy-MM-dd",
                          "enableDate": true,
                          "enableTime": false,
                          "widget": {
                            "type": "calendar",
                            "displayInTimezone": "viewer",
                            "locale": "ja",
                            "useLocaleSettings": false,
                            "allowInput": true,
                            "mode": "single",
                            "enableTime": false,
                            "noCalendar": false,
                            "format": "yyyy-MM-dd",
                            "hourIncrement": 1,
                            "minuteIncrement": 1,
                            "time_24hr": false,
                            "minDate": null,
                            "maxDate": null
                          },
                          "calculateValue": "if (typeof cfGlobal !== 'undefined' && cfGlobal.isNewNote) { value = moment().startOf('day').toISOString(); }",
                          "allowCalculateOverride": false,
                          "description": "calculateValue + isNewNote ガード — defaultDate だと公開日の値が FieldJson に焼き込まれ凍結します"
                        }
                      ],
                      "width": 4,
                      "offset": 0,
                      "push": 0,
                      "pull": 0,
                      "size": "md",
                      "currentWidth": 4
                    }
                  ]
                }
              ]
            }
          ]
        },
        {
          "key": "tabLayout",
          "label": "🧱 レイアウト",
          "components": [
            {
              "type": "htmlelement",
              "tag": "div",
              "className": "ks-note",
              "key": "introLayout",
              "content": "グルーピングは <code>panel / columns / fieldset / well / table / tabs</code> (データはフラットのまま) を使います。<code>container</code> は <code>data.&lt;container&gt;.&lt;key&gt;</code> にネストし保存モデルと不整合のため<strong>原則禁止</strong>です。",
              "input": false,
              "tableView": false
            },
            {
              "type": "panel",
              "key": "ksBlockLayoutDemos",
              "title": "🧱 グルーピング部品 — fieldset / well / table / panel",
              "customClass": "ks-block",
              "collapsible": true,
              "collapsed": false,
              "input": false,
              "tableView": false,
              "components": [
                {
                  "type": "fieldset",
                  "key": "fieldsetDemo",
                  "legend": "フィールドセット (fieldset)",
                  "input": false,
                  "tableView": false,
                  "components": [
                    {
                      "type": "textfield",
                      "key": "insideFieldset",
                      "label": "fieldset 内のフィールド",
                      "input": true,
                      "tableView": false
                    }
                  ]
                },
                {
                  "type": "well",
                  "key": "wellDemo",
                  "input": false,
                  "tableView": false,
                  "components": [
                    {
                      "type": "textfield",
                      "key": "insideWell",
                      "label": "well 内のフィールド",
                      "input": true,
                      "tableView": false
                    }
                  ]
                },
                {
                  "type": "table",
                  "key": "tableDemo",
                  "label": "テーブル (table)",
                  "input": false,
                  "tableView": false,
                  "numRows": 2,
                  "numCols": 2,
                  "striped": true,
                  "bordered": true,
                  "cloneRows": false,
                  "header": [],
                  "caption": "",
                  "rows": [
                    [
                      {
                        "components": [
                          {
                            "type": "textfield",
                            "key": "tblCell11",
                            "label": "項目 A",
                            "input": true,
                            "tableView": false
                          }
                        ]
                      },
                      {
                        "components": [
                          {
                            "type": "textfield",
                            "key": "tblCell12",
                            "label": "項目 B",
                            "input": true,
                            "tableView": false
                          }
                        ]
                      }
                    ],
                    [
                      {
                        "components": [
                          {
                            "type": "textfield",
                            "key": "tblCell21",
                            "label": "項目 C",
                            "input": true,
                            "tableView": false
                          }
                        ]
                      },
                      {
                        "components": [
                          {
                            "type": "textfield",
                            "key": "tblCell22",
                            "label": "項目 D",
                            "input": true,
                            "tableView": false
                          }
                        ]
                      }
                    ]
                  ]
                },
                {
                  "type": "panel",
                  "key": "panelDemo",
                  "title": "折りたたみパネル (panel collapsible)",
                  "collapsible": true,
                  "collapsed": true,
                  "input": false,
                  "tableView": false,
                  "components": [
                    {
                      "type": "textarea",
                      "key": "insidePanel",
                      "label": "panel 内のフィールド",
                      "rows": 2,
                      "input": true,
                      "tableView": false
                    }
                  ]
                }
              ]
            },
            {
              "type": "panel",
              "key": "ksBlockPicker",
              "title": "🪟 modalEdit パネル (ダイアログピッカー)",
              "customClass": "ks-block",
              "collapsible": true,
              "collapsed": false,
              "input": false,
              "tableView": false,
              "components": [
                {
                  "type": "htmlelement",
                  "tag": "div",
                  "className": "ks-note",
                  "key": "pickerNote",
                  "content": "panel に <code>modalEdit:true</code> を付けるとモーダルダイアログになります。閉じるときは <code>[ref=\"modalSave\"]</code> を programmatic click (modalClose は破棄確認が出る)。一時フィールドは <code>setValue(null) + delete data + delete root.data</code> の 3 段リセットで永続化を防ぎます。",
                  "input": false,
                  "tableView": false
                },
                {
                  "type": "columns",
                  "key": "pickerCols",
                  "label": "Columns",
                  "input": false,
                  "tableView": false,
                  "columns": [
                    {
                      "components": [
                        {
                          "type": "textfield",
                          "key": "pickedPerson",
                          "label": "選択された担当者",
                          "input": true,
                          "tableView": false,
                          "disabled": true
                        }
                      ],
                      "width": 6,
                      "offset": 0,
                      "push": 0,
                      "pull": 0,
                      "size": "md",
                      "currentWidth": 6
                    },
                    {
                      "components": [
                        {
                          "type": "panel",
                          "key": "pickerPanel",
                          "title": "担当者を選択",
                          "label": "担当者を選択",
                          "modalEdit": true,
                          "input": false,
                          "collapsible": false,
                          "tableView": false,
                          "customClass": "lc-picker",
                          "components": [
                            {
                              "type": "select",
                              "key": "pickerTmp",
                              "label": "担当者 (一時フィールド・保存されません ※設定「persistent:false / Client-only の項目を保存しない」が はい のとき)",
                              "input": true,
                              "tableView": false,
                              "widget": "choicesjs",
                              "dataSrc": "values",
                              "data": {
                                "values": [
                                  {
                                    "label": "山田太郎",
                                    "value": "山田太郎"
                                  },
                                  {
                                    "label": "佐藤花子",
                                    "value": "佐藤花子"
                                  },
                                  {
                                    "label": "鈴木一郎",
                                    "value": "鈴木一郎"
                                  },
                                  {
                                    "label": "高橋美咲",
                                    "value": "高橋美咲"
                                  },
                                  {
                                    "label": "田中健太",
                                    "value": "田中健太"
                                  }
                                ]
                              },
                              "defaultValue": "",
                              "template": "<span>{{ item.label }}</span>",
                              "searchEnabled": true,
                              "lazyLoad": true,
                              "persistent": false
                            },
                            {
                              "type": "button",
                              "key": "pickerSet",
                              "label": "✓ セットする",
                              "action": "custom",
                              "theme": "primary",
                              "input": true,
                              "tableView": false,
                              "showValidations": false,
                              "custom": "var picked = data.pickerTmp || '';\nif (!picked) { alert('担当者を選択してください'); return; }\nform.getComponent('pickedPerson').setValue(picked);\n// 一時フィールドはサーバーに送らない — 3 段リセットしてから modalSave (modalClose だと破棄確認が出る)\nvar selComp = form.getComponent('pickerTmp');\nif (selComp) { try { selComp.setValue(null); } catch (e) {} }\ntry { delete data.pickerTmp; } catch (e) {}\ntry { if (root && root.data) { delete root.data.pickerTmp; } } catch (e) {}\nvar modalRoot = instance.element ? instance.element.closest('.formio-dialog-content') : null;\nvar saveBtn = modalRoot ? modalRoot.querySelector('[ref=\"modalSave\"]') : null;\nif (saveBtn) { saveBtn.click(); }"
                            }
                          ]
                        }
                      ],
                      "width": 6,
                      "offset": 0,
                      "push": 0,
                      "pull": 0,
                      "size": "md",
                      "currentWidth": 6
                    }
                  ]
                }
              ]
            },
            {
              "type": "panel",
              "key": "ksBlockContent",
              "title": "📄 content と htmlelement",
              "customClass": "ks-block",
              "collapsible": true,
              "collapsed": false,
              "input": false,
              "tableView": false,
              "components": [
                {
                  "type": "content",
                  "key": "contentDemo",
                  "label": "Content",
                  "input": false,
                  "tableView": false,
                  "html": "<p>こんにちは!! この <strong>content</strong> コンポーネントは HTML エディタで自由に記入できます。<code>&lt;script&gt;</code> はサニタイズされ実行されないため、ロジックは customConditional / calculateValue / logic[] に書きます。</p>"
                },
                {
                  "type": "htmlelement",
                  "key": "htmlDemo",
                  "tag": "div",
                  "input": false,
                  "tableView": false,
                  "refreshOnChange": true,
                  "content": "<div style='padding:8px 12px;background:#fff8e1;border-radius:4px;'>htmlelement は EJS テンプレートが使えます — こんにちは <strong>{{ data.lastName || '(姓を入力してください)' }}</strong> さん (refreshOnChange:true でライブ更新)</div>"
                }
              ]
            }
          ]
        },
        {
          "key": "tabGrid",
          "label": "📋 グリッド・署名",
          "components": [
            {
              "type": "htmlelement",
              "tag": "div",
              "className": "ks-note",
              "key": "introGrid",
              "content": "繰り返し行は <code>datagrid</code> (常時展開) / <code>editgrid</code> (行単位編集)。EditGrid のカスタム行テンプレートには参照モード用に <code>!instance.options.readOnly && !instance.disabled</code> ガードを入れます。<code>signature</code> は Notes アイテム同期対象外です。",
              "input": false,
              "tableView": false
            },
            {
              "type": "panel",
              "key": "ksBlockGrids",
              "title": "📋 繰り返し行 — datagrid / editgrid",
              "customClass": "ks-block",
              "collapsible": true,
              "collapsed": false,
              "input": false,
              "tableView": false,
              "components": [
                {
                  "type": "datagrid",
                  "key": "carsGrid",
                  "label": "車両 (datagrid)",
                  "input": true,
                  "tableView": false,
                  "reorder": true,
                  "addAnother": "＋ 行を追加",
                  "defaultValue": [
                    {
                      "carMake": "トヨタ",
                      "carModel": "カローラ",
                      "carYear": 2022
                    },
                    {
                      "carMake": "ホンダ",
                      "carModel": "フィット",
                      "carYear": 2024
                    }
                  ],
                  "components": [
                    {
                      "type": "textfield",
                      "key": "carMake",
                      "label": "メーカー",
                      "input": true,
                      "tableView": true
                    },
                    {
                      "type": "textfield",
                      "key": "carModel",
                      "label": "モデル",
                      "input": true,
                      "tableView": true
                    },
                    {
                      "type": "number",
                      "key": "carYear",
                      "label": "製造年",
                      "input": true,
                      "tableView": true
                    }
                  ]
                },
                {
                  "type": "editgrid",
                  "key": "contactsGrid",
                  "label": "連絡先 (editgrid)",
                  "input": true,
                  "tableView": false,
                  "addAnother": "＋ 連絡先を追加",
                  "saveRow": "✓ 保存",
                  "removeRow": "削除",
                  "rowDrafts": false,
                  "defaultValue": [
                    {
                      "contactName": "山田太郎",
                      "contactType": "work",
                      "contactMemo": "内線 1234"
                    },
                    {
                      "contactName": "佐藤花子",
                      "contactType": "family",
                      "contactMemo": ""
                    }
                  ],
                  "templates": {
                    "header": "<div class='row' style='background:#f8f9fa;padding:8px;border-radius:6px 6px 0 0;font-weight:bold;'><div class='col-sm-4'>氏名</div><div class='col-sm-3'>種別</div><div class='col-sm-3'>メモ</div><div class='col-sm-2'>操作</div></div>",
                    "row": "<div class='row' style='align-items:center;'><div class='col-sm-4' style='overflow:hidden;text-overflow:ellipsis;white-space:nowrap;'>{{ row.contactName || '(未入力)' }}</div><div class='col-sm-3'>{% if(row.contactType==='work'){ %}🏢 職場{% } else if(row.contactType==='family'){ %}🏠 家族{% } else { %}⭕ その他{% } %}</div><div class='col-sm-3' style='overflow:hidden;text-overflow:ellipsis;white-space:nowrap;'>{{ row.contactMemo || '-' }}</div><div class='col-sm-2'>{% if (!instance.options.readOnly && !instance.disabled) { %}<button class='btn btn-sm btn-outline-primary editRow' type='button'>✏️</button> <button class='btn btn-sm btn-outline-danger removeRow' type='button'>🗑️</button>{% } %}</div></div>"
                  },
                  "components": [
                    {
                      "type": "columns",
                      "key": "contactRowCols",
                      "label": "Columns",
                      "input": false,
                      "tableView": false,
                      "columns": [
                        {
                          "components": [
                            {
                              "type": "textfield",
                              "key": "contactName",
                              "label": "氏名",
                              "input": true,
                              "tableView": true,
                              "validate": {
                                "required": true
                              }
                            }
                          ],
                          "width": 4,
                          "offset": 0,
                          "push": 0,
                          "pull": 0,
                          "size": "md",
                          "currentWidth": 4
                        },
                        {
                          "components": [
                            {
                              "type": "select",
                              "key": "contactType",
                              "label": "種別",
                              "input": true,
                              "tableView": false,
                              "widget": "choicesjs",
                              "dataSrc": "values",
                              "data": {
                                "values": [
                                  {
                                    "label": "職場",
                                    "value": "work"
                                  },
                                  {
                                    "label": "家族",
                                    "value": "family"
                                  },
                                  {
                                    "label": "その他",
                                    "value": "other"
                                  }
                                ]
                              },
                              "defaultValue": "work",
                              "template": "<span>{{ item.label }}</span>",
                              "searchEnabled": true,
                              "lazyLoad": true
                            }
                          ],
                          "width": 4,
                          "offset": 0,
                          "push": 0,
                          "pull": 0,
                          "size": "md",
                          "currentWidth": 4
                        },
                        {
                          "components": [
                            {
                              "type": "textfield",
                              "key": "contactMemo",
                              "label": "メモ",
                              "input": true,
                              "tableView": true
                            }
                          ],
                          "width": 4,
                          "offset": 0,
                          "push": 0,
                          "pull": 0,
                          "size": "md",
                          "currentWidth": 4
                        }
                      ]
                    }
                  ]
                }
              ]
            },
            {
              "type": "panel",
              "key": "ksBlockSurvey",
              "title": "📝 survey と signature",
              "customClass": "ks-block",
              "collapsible": true,
              "collapsed": false,
              "input": false,
              "tableView": false,
              "components": [
                {
                  "type": "survey",
                  "key": "surveyDemo",
                  "label": "アンケート (survey・値はオブジェクトで保存)",
                  "input": true,
                  "tableView": false,
                  "questions": [
                    {
                      "label": "使いやすさ",
                      "value": "usability",
                      "tooltip": ""
                    },
                    {
                      "label": "機能の充実度",
                      "value": "features",
                      "tooltip": ""
                    },
                    {
                      "label": "ドキュメント",
                      "value": "docs",
                      "tooltip": ""
                    }
                  ],
                  "values": [
                    {
                      "label": "満足",
                      "value": "3",
                      "tooltip": ""
                    },
                    {
                      "label": "普通",
                      "value": "2",
                      "tooltip": ""
                    },
                    {
                      "label": "不満",
                      "value": "1",
                      "tooltip": ""
                    }
                  ]
                },
                {
                  "type": "signature",
                  "key": "signatureField",
                  "label": "署名 (signature)",
                  "input": true,
                  "tableView": false,
                  "footer": "上の枠内に署名してください",
                  "width": "100%",
                  "height": "150px",
                  "description": "⚠ signature は Notes アイテムへの同期対象外です (submit も同様)"
                }
              ]
            }
          ]
        },
        {
          "key": "tabLogic",
          "label": "⚙️ ロジック・計算",
          "components": [
            {
              "type": "htmlelement",
              "tag": "div",
              "className": "ks-note",
              "key": "introLogic",
              "content": "<code>&lt;script&gt;</code> 直書きはサニタイズで実行されません。ロジックは Form.io 拡張点 <code>customConditional / calculateValue / validate.custom / logic[]</code> + button (action:custom) に書きます。",
              "input": false,
              "tableView": false
            },
            {
              "type": "panel",
              "key": "ksBlockCond",
              "title": "👁 customConditional (条件表示)",
              "customClass": "ks-block",
              "collapsible": true,
              "collapsed": false,
              "input": false,
              "tableView": false,
              "components": [
                {
                  "type": "checkbox",
                  "key": "showExtra",
                  "label": "詳細メモを表示する",
                  "input": true,
                  "tableView": false,
                  "defaultValue": false
                },
                {
                  "type": "textarea",
                  "key": "extraNote",
                  "label": "詳細メモ (showExtra が ON のとき表示)",
                  "rows": 2,
                  "input": true,
                  "tableView": false,
                  "customConditional": "show = data.showExtra === true;"
                },
                {
                  "type": "htmlelement",
                  "tag": "div",
                  "key": "editableBadge",
                  "input": false,
                  "tableView": false,
                  "content": "<div style='display:inline-block;padding:6px 14px;background:#e8f5e9;border:1px solid #a5d6a7;border-radius:16px;color:#2e7d32;font-size:12px;margin-bottom:10px;'>✏️ 編集モードです — customConditional: show = cfGlobal.editable === true</div>",
                  "customConditional": "show = (typeof cfGlobal === 'undefined') || cfGlobal.editable === true;"
                },
                {
                  "type": "htmlelement",
                  "tag": "div",
                  "key": "readonlyBadge",
                  "input": false,
                  "tableView": false,
                  "content": "<div style='display:inline-block;padding:6px 14px;background:#eceff1;border:1px solid #b0bec5;border-radius:16px;color:#546e7a;font-size:12px;margin-bottom:10px;'>👁 参照モードです — customConditional: show = cfGlobal.editable === false</div>",
                  "customConditional": "show = (typeof cfGlobal !== 'undefined') && cfGlobal.editable === false;"
                }
              ]
            },
            {
              "type": "panel",
              "key": "ksBlockCalc",
              "title": "🧮 calculateValue (自動計算)",
              "customClass": "ks-block",
              "collapsible": true,
              "collapsed": false,
              "input": false,
              "tableView": false,
              "components": [
                {
                  "type": "columns",
                  "key": "calcCols",
                  "label": "Columns",
                  "input": false,
                  "tableView": false,
                  "columns": [
                    {
                      "components": [
                        {
                          "type": "number",
                          "key": "unitPrice",
                          "label": "単価",
                          "input": true,
                          "tableView": false,
                          "defaultValue": 0
                        }
                      ],
                      "width": 4,
                      "offset": 0,
                      "push": 0,
                      "pull": 0,
                      "size": "md",
                      "currentWidth": 4
                    },
                    {
                      "components": [
                        {
                          "type": "number",
                          "key": "quantity",
                          "label": "数量",
                          "input": true,
                          "tableView": false,
                          "defaultValue": 0
                        }
                      ],
                      "width": 4,
                      "offset": 0,
                      "push": 0,
                      "pull": 0,
                      "size": "md",
                      "currentWidth": 4
                    },
                    {
                      "components": [
                        {
                          "type": "currency",
                          "key": "totalAmount",
                          "label": "合計 (単価×数量・自動計算)",
                          "currency": "JPY",
                          "input": true,
                          "tableView": false,
                          "disabled": true,
                          "calculateValue": "value = (data.unitPrice || 0) * (data.quantity || 0);",
                          "allowCalculateOverride": false,
                          "description": "連動計算は allowCalculateOverride:false (既定) — true だとロード後に再計算されません"
                        }
                      ],
                      "width": 4,
                      "offset": 0,
                      "push": 0,
                      "pull": 0,
                      "size": "md",
                      "currentWidth": 4
                    }
                  ]
                }
              ]
            },
            {
              "type": "panel",
              "key": "ksBlockValidate",
              "title": "🛡 validate.custom (未申請ガード付き)",
              "customClass": "ks-block",
              "collapsible": true,
              "collapsed": false,
              "input": false,
              "tableView": false,
              "components": [
                {
                  "type": "datetime",
                  "key": "deliveryDate",
                  "label": "希望日 (申請時のみ「3 日後以降」を検証)",
                  "input": true,
                  "tableView": false,
                  "format": "yyyy-MM-dd",
                  "enableDate": true,
                  "enableTime": false,
                  "widget": {
                    "type": "calendar",
                    "displayInTimezone": "viewer",
                    "locale": "ja",
                    "useLocaleSettings": false,
                    "allowInput": true,
                    "mode": "single",
                    "enableTime": false,
                    "noCalendar": false,
                    "format": "yyyy-MM-dd",
                    "hourIncrement": 1,
                    "minuteIncrement": 1,
                    "time_24hr": false,
                    "minDate": null,
                    "maxDate": null
                  },
                  "validate": {
                    "custom": "var isDraft = (typeof cfGlobal === 'undefined') ? true : (cfGlobal.applicationNo === '' && (cfGlobal.statusNo === '' || cfGlobal.statusNo === '0'));\nif (!isDraft || !input) { valid = true; } else { var minD = moment().startOf('day').add(3, 'days'); valid = moment(input).startOf('day').isSameOrAfter(minD) ? true : '希望日は 3 日後以降を指定してください'; }"
                  },
                  "description": "時間相対チェックは未申請ガード必須 — ガード無しだと日付経過後の承認時に全承認者がブロックされます (minDate には書かない)"
                }
              ]
            },
            {
              "type": "panel",
              "key": "ksBlockEvent",
              "title": "⚡ logic[] (イベント駆動) と type:\"hidden\"",
              "customClass": "ks-block",
              "collapsible": true,
              "collapsed": false,
              "input": false,
              "tableView": false,
              "components": [
                {
                  "type": "columns",
                  "key": "eventCols",
                  "label": "Columns",
                  "input": false,
                  "tableView": false,
                  "columns": [
                    {
                      "components": [
                        {
                          "type": "button",
                          "key": "btnEmitEvent",
                          "label": "⚡ イベント発火 (action:event)",
                          "action": "event",
                          "event": "ksDemoEvent",
                          "theme": "info",
                          "input": true,
                          "tableView": false,
                          "showValidations": false
                        }
                      ],
                      "width": 4,
                      "offset": 0,
                      "push": 0,
                      "pull": 0,
                      "size": "md",
                      "currentWidth": 4
                    },
                    {
                      "components": [
                        {
                          "type": "textfield",
                          "key": "eventLog",
                          "label": "logic[] trigger:event で受信",
                          "input": true,
                          "tableView": false,
                          "disabled": true,
                          "persistent": false,
                          "logic": [
                            {
                              "name": "onKsDemoEvent",
                              "trigger": {
                                "type": "event",
                                "event": "ksDemoEvent"
                              },
                              "actions": [
                                {
                                  "name": "setVal",
                                  "type": "value",
                                  "value": "value = 'イベント受信: ' + new Date().toLocaleTimeString('ja-JP');"
                                }
                              ]
                            }
                          ]
                        }
                      ],
                      "width": 8,
                      "offset": 0,
                      "push": 0,
                      "pull": 0,
                      "size": "md",
                      "currentWidth": 8
                    }
                  ]
                },
                {
                  "type": "columns",
                  "key": "hiddenCols",
                  "label": "Columns",
                  "input": false,
                  "tableView": false,
                  "columns": [
                    {
                      "components": [
                        {
                          "type": "button",
                          "key": "btnSetHidden",
                          "label": "隠しフィールドに書込",
                          "action": "custom",
                          "theme": "secondary",
                          "input": true,
                          "tableView": false,
                          "showValidations": false,
                          "custom": "form.getComponent('hiddenStamp').setValue('set @ ' + new Date().toLocaleTimeString('ja-JP'));"
                        }
                      ],
                      "width": 4,
                      "offset": 0,
                      "push": 0,
                      "pull": 0,
                      "size": "md",
                      "currentWidth": 4
                    },
                    {
                      "components": [
                        {
                          "type": "htmlelement",
                          "key": "hiddenStampView",
                          "tag": "div",
                          "input": false,
                          "tableView": false,
                          "refreshOnChange": true,
                          "content": "<div style='padding:8px 12px;background:#f5f5f5;border-radius:4px;margin-top:28px;'>hidden の現在値: <strong>{{ data.hiddenStamp || '(未設定)' }}</strong> — setValue する隠しフィールドは <code>type:\"hidden\"</code> を使います (textfield+hidden:true は clearOnHide で送信時にクリアされます)</div>"
                        }
                      ],
                      "width": 8,
                      "offset": 0,
                      "push": 0,
                      "pull": 0,
                      "size": "md",
                      "currentWidth": 8
                    }
                  ]
                }
              ]
            },
            {
              "type": "panel",
              "key": "ksBlockLifecycle",
              "title": "🔔 ライフサイクルイベント — logic[] trigger:event の initialized / change / blur / focus / render / error は「フォーム全体」のイベント",
              "customClass": "ks-block",
              "collapsible": true,
              "collapsed": false,
              "input": false,
              "tableView": false,
              "components": [
                {
                  "type": "htmlelement",
                  "tag": "div",
                  "className": "ks-note",
                  "key": "lcIntro",
                  "input": false,
                  "tableView": false,
                  "content": "<strong>ビルダーでの設定:</strong> ロジック → トリガー「タイプ」で <code>イベント名</code> を選ぶと、その下に「イベント名」の<strong>自由入力欄</strong>が出ます (候補リストは出ません)。そこに Form.io 内部イベント名を<strong>小文字のまま</strong>手入力すると購読できます (<code>initialized</code> / <code>change</code> / <code>blur</code> / <code>focus</code> / <code>render</code> / <code>error</code> / <code>submit</code>)。大文字小文字は区別され <code>Initialized</code> では発火しません。JSON では <code>\"trigger\": {\"type\": \"event\", \"event\": \"blur\"}</code>。<br><strong>対象は常にフォーム全体:</strong> Form.io の <code>on()</code> はフォーム共有の emitter に登録されるため、<code>blur</code> を購読した項目は<strong>どの項目</strong>がフォーカスを失っても発火します (この項目のイベントだけ、ではありません)。自分の項目に絞るには customAction でイベント引数 <code>result[0]</code> を見ます: blur / focus は <code>result[0] === instance</code> (対象項目のインスタンス)、change は <code>result[0].changed.component.key</code>。<br><strong>initialized</strong> は Form.io インスタンスごとに 1 回 (保存データの適用後、最初の change の直後)。consentFlow では部分更新のたびに Form.io が作り直されるので<strong>再発火</strong>します。「開いたとき 1 回だけ」は下の <code>scriptBlock</code> + <code>cfAPI.form.onOpen</code> を使い、initialized には書かないのが安全です。<br><strong>ループ注意:</strong> customAction の評価後の <code>value</code> は自分の値として setValue されます。<code>change</code> を購読した customAction で <code>value</code> を書き換えると change → 再発火のループになります。表示はこの例のように DOM へ直書きするか、blur など change 以外のイベントで別項目へ書きます。"
                },
                {
                  "type": "htmlelement",
                  "tag": "div",
                  "key": "lcSpec",
                  "input": false,
                  "tableView": false,
                  "content": "<table class='ks-spec'><tr><th>イベント名 (trigger.event)</th><th>発火タイミング (Form.io 4.14 / consentFlow 実測)</th><th>対象</th><th>customAction の result[0]</th></tr><tr><td>initialized</td><td>保存データ適用後、最初の change 直後に 1 回。部分更新で Form.io が作り直されると再発火</td><td>フォーム全体</td><td>なし (undefined)</td></tr><tr><td>change</td><td>どの項目でも値が変わるたび (ロード時の submission 適用でも 1 回。連続した変更は 1 回に合体)</td><td>フォーム全体</td><td><code>{ data, changed: { component, instance, value, flags } }</code>。変わった項目は <code>result[0].changed.component.key</code>、ロード由来は <code>flags.fromSubmission</code></td></tr><tr><td>componentChange</td><td>項目単位の値変更 (change の直前)</td><td>フォーム全体</td><td><code>{ instance, component, value, flags }</code></td></tr><tr><td>blur / focus</td><td>入力欄のフォーカス移動</td><td>フォーム全体</td><td>対象項目のインスタンス (<code>result[0].key</code>、<code>result[0] === instance</code> で自分だけに絞る)</td></tr><tr><td>render</td><td>描画 (attach) 完了。<strong>initialized より前</strong></td><td>フォーム全体</td><td>描画された要素</td></tr><tr><td>error</td><td><code>form.submit()</code> の検証に失敗したとき。<strong>consentFlow の保存 / 申請 / 承認ボタンは先に <code>checkValidity()</code> で止めてエラー一覧を自分で出すので発火しない</strong> (差戻し / 取戻し / 否認は検証なしで submit() を呼ぶ)。入力エラーの検知は <code>validate.custom</code> か <code>change</code> + <code>instance.root.errors</code> で</td><td>フォーム全体</td><td>エラー配列 (<code>result[0][i].message</code>)</td></tr><tr><td>submit</td><td><code>form.submit()</code> が検証に通って完了したとき (非同期・数十 ms 後)。<strong>consentFlow の保存 / 申請 / 承認ボタンは <code>form.submit()</code> を呼んだ直後に同期で XPages の送信を始めるため、この event は間に合わず通常は発火しない</strong>。「保存直前に 1 回」は使えない (保存後の処理はサーバー側フック <code>customExtSaveOnClickAfter</code> の領域)</td><td>フォーム全体</td><td>submission</td></tr><tr><td>任意の名前</td><td>button <code>action:\"event\"</code> や <code>form.emit('名前')</code> で手動発火 (上の ⚡ パネルの例)</td><td>フォーム全体</td><td>emit に渡した引数</td></tr><tr><td colspan='4'><strong>載せる場所の注意:</strong> logic[] は textfield / hidden など<strong>入力系コンポーネント</strong>に置く。htmlelement / content に置くと <code>trigger.event</code> が i18n 翻訳されてから購読され、日本語 UI では <code>error</code> / <code>submit</code> が辞書語 (「処理前に以下のエラーを修正してください。」/「フォームの送信」) に化けて永久に発火しない (Form.io 4.14 実測)。</td></tr></table>"
                },
                {
                  "type": "columns",
                  "key": "lcCols1",
                  "label": "Columns",
                  "input": false,
                  "tableView": false,
                  "columns": [
                    {
                      "components": [
                        {
                          "type": "textfield",
                          "key": "lcFieldA",
                          "label": "A: 入力して focus / blur してみる",
                          "input": true,
                          "tableView": false,
                          "persistent": false,
                          "description": "この項目の logic[] は trigger.event=blur で、result[0] === instance のときだけ右の「A 自身の blur 回数」を進める (自分だけに絞る書き方)",
                          "logic": [
                            {
                              "name": "ownBlurOnly",
                              "trigger": {
                                "type": "event",
                                "event": "blur"
                              },
                              "actions": [
                                {
                                  "name": "countOwnBlur",
                                  "type": "customAction",
                                  "customAction": "// blur はフォーム全体で発火する。result[0] (blur した項目のインスタンス) が自分のときだけ処理する\nif (result[0] === instance) {\n  var c = instance.root.getComponent('lcOwnBlurCount');\n  if (c) { c.setValue(String((parseInt(c.dataValue, 10) || 0) + 1)); }\n}\n// value には触らない (触ると自分の値が setValue される)"
                                }
                              ]
                            }
                          ]
                        }
                      ],
                      "width": 4,
                      "offset": 0,
                      "push": 0,
                      "pull": 0,
                      "size": "md",
                      "currentWidth": 4
                    },
                    {
                      "components": [
                        {
                          "type": "textfield",
                          "key": "lcFieldB",
                          "label": "B: こちらを blur しても「全体の blur 回数」は進む",
                          "input": true,
                          "tableView": false,
                          "persistent": false
                        }
                      ],
                      "width": 4,
                      "offset": 0,
                      "push": 0,
                      "pull": 0,
                      "size": "md",
                      "currentWidth": 4
                    },
                    {
                      "components": [
                        {
                          "type": "textfield",
                          "key": "lcOwnBlurCount",
                          "label": "A 自身の blur 回数 (result[0] === instance)",
                          "input": true,
                          "tableView": false,
                          "disabled": true,
                          "persistent": false,
                          "defaultValue": "0"
                        }
                      ],
                      "width": 4,
                      "offset": 0,
                      "push": 0,
                      "pull": 0,
                      "size": "md",
                      "currentWidth": 4
                    }
                  ]
                },
                {
                  "type": "columns",
                  "key": "lcCols2",
                  "label": "Columns",
                  "input": false,
                  "tableView": false,
                  "columns": [
                    {
                      "components": [
                        {
                          "type": "textfield",
                          "key": "lcAnyBlurCount",
                          "label": "全体の blur 回数 (この項目の logic: trigger.event=blur → action:value)",
                          "input": true,
                          "tableView": false,
                          "disabled": true,
                          "persistent": false,
                          "defaultValue": "0",
                          "logic": [
                            {
                              "name": "anyBlur",
                              "trigger": {
                                "type": "event",
                                "event": "blur"
                              },
                              "actions": [
                                {
                                  "name": "inc",
                                  "type": "value",
                                  "value": "value = String((parseInt(value, 10) || 0) + 1);"
                                }
                              ]
                            }
                          ]
                        }
                      ],
                      "width": 4,
                      "offset": 0,
                      "push": 0,
                      "pull": 0,
                      "size": "md",
                      "currentWidth": 4
                    },
                    {
                      "components": [
                        {
                          "type": "textfield",
                          "key": "lcLastBlurKey",
                          "label": "最後に blur した項目 (result[0].key)",
                          "input": true,
                          "tableView": false,
                          "disabled": true,
                          "persistent": false,
                          "logic": [
                            {
                              "name": "lastBlur",
                              "trigger": {
                                "type": "event",
                                "event": "blur"
                              },
                              "actions": [
                                {
                                  "name": "setKey",
                                  "type": "value",
                                  "value": "value = (result[0] && result[0].key) ? result[0].key : '';"
                                }
                              ]
                            }
                          ]
                        }
                      ],
                      "width": 4,
                      "offset": 0,
                      "push": 0,
                      "pull": 0,
                      "size": "md",
                      "currentWidth": 4
                    },
                    {
                      "components": [
                        {
                          "type": "button",
                          "key": "btnLcClear",
                          "label": "🧹 ログをクリア",
                          "action": "custom",
                          "theme": "secondary",
                          "size": "sm",
                          "input": true,
                          "tableView": false,
                          "showValidations": false,
                          "customClass": "ks-run-btn",
                          "custom": "window._ksLcBuf = [];\nvar box = document.getElementById('lcEventLogBox');\nif (box) { box.textContent = '(クリアしました。項目を触るとここに流れます)'; }"
                        }
                      ],
                      "width": 4,
                      "offset": 0,
                      "push": 0,
                      "pull": 0,
                      "size": "md",
                      "currentWidth": 4
                    }
                  ]
                },
                {
                  "type": "hidden",
                  "key": "lcEventTap",
                  "label": "イベント購読用 (hidden)",
                  "input": true,
                  "tableView": false,
                  "persistent": false,
                  "description": "logic[] を載せるだけの非表示項目。htmlelement / content に logic を載せると trigger.event が i18n 翻訳されて error / submit が購読できないため、入力系コンポーネントに置く",
                  "logic": [
                    {
                      "name": "on_initialized",
                      "trigger": {
                        "type": "event",
                        "event": "initialized"
                      },
                      "actions": [
                        {
                          "name": "log",
                          "type": "customAction",
                          "customAction": "// trigger.event = 'initialized' — フォーム全体のイベント。result = イベント引数の配列 (result[0] が第 1 引数)\nvar push = window._ksLcPush || (window._ksLcPush = function (ev, detail) {\n  var buf = window._ksLcBuf = window._ksLcBuf || [];\n  buf.unshift(new Date().toLocaleTimeString('ja-JP') + '  ' + ev + (detail ? '  ' + detail : ''));\n  if (buf.length > 30) { buf.length = 30; }\n  var box = document.getElementById('lcEventLogBox');\n  if (box) { box.textContent = buf.join('\\n'); }\n});\nwindow._ksLcInit = (window._ksLcInit || 0) + 1;\npush('initialized', '#' + window._ksLcInit + '  result[0]=' + typeof result[0] + '  (Form.io インスタンスごとに 1 回。部分更新で作り直されると再発火)');"
                        }
                      ]
                    },
                    {
                      "name": "on_render",
                      "trigger": {
                        "type": "event",
                        "event": "render"
                      },
                      "actions": [
                        {
                          "name": "log",
                          "type": "customAction",
                          "customAction": "// trigger.event = 'render' — フォーム全体のイベント。result = イベント引数の配列 (result[0] が第 1 引数)\nvar push = window._ksLcPush || (window._ksLcPush = function (ev, detail) {\n  var buf = window._ksLcBuf = window._ksLcBuf || [];\n  buf.unshift(new Date().toLocaleTimeString('ja-JP') + '  ' + ev + (detail ? '  ' + detail : ''));\n  if (buf.length > 30) { buf.length = 30; }\n  var box = document.getElementById('lcEventLogBox');\n  if (box) { box.textContent = buf.join('\\n'); }\n});\npush('render', '(描画 attach 完了。initialized より前に発火)');"
                        }
                      ]
                    },
                    {
                      "name": "on_change",
                      "trigger": {
                        "type": "event",
                        "event": "change"
                      },
                      "actions": [
                        {
                          "name": "log",
                          "type": "customAction",
                          "customAction": "// trigger.event = 'change' — フォーム全体のイベント。result = イベント引数の配列 (result[0] が第 1 引数)\nvar push = window._ksLcPush || (window._ksLcPush = function (ev, detail) {\n  var buf = window._ksLcBuf = window._ksLcBuf || [];\n  buf.unshift(new Date().toLocaleTimeString('ja-JP') + '  ' + ev + (detail ? '  ' + detail : ''));\n  if (buf.length > 30) { buf.length = 30; }\n  var box = document.getElementById('lcEventLogBox');\n  if (box) { box.textContent = buf.join('\\n'); }\n});\nvar c = result[0] && result[0].changed;\npush('change', c && c.component ? 'key=' + c.component.key + (c.flags && c.flags.fromSubmission ? '  (fromSubmission)' : '') : '(保存データの適用 / 計算値: changed なし)');"
                        }
                      ]
                    },
                    {
                      "name": "on_blur",
                      "trigger": {
                        "type": "event",
                        "event": "blur"
                      },
                      "actions": [
                        {
                          "name": "log",
                          "type": "customAction",
                          "customAction": "// trigger.event = 'blur' — フォーム全体のイベント。result = イベント引数の配列 (result[0] が第 1 引数)\nvar push = window._ksLcPush || (window._ksLcPush = function (ev, detail) {\n  var buf = window._ksLcBuf = window._ksLcBuf || [];\n  buf.unshift(new Date().toLocaleTimeString('ja-JP') + '  ' + ev + (detail ? '  ' + detail : ''));\n  if (buf.length > 30) { buf.length = 30; }\n  var box = document.getElementById('lcEventLogBox');\n  if (box) { box.textContent = buf.join('\\n'); }\n});\npush('blur', 'key=' + (result[0] && result[0].key) + '  ← どの項目の blur でも発火する');"
                        }
                      ]
                    },
                    {
                      "name": "on_focus",
                      "trigger": {
                        "type": "event",
                        "event": "focus"
                      },
                      "actions": [
                        {
                          "name": "log",
                          "type": "customAction",
                          "customAction": "// trigger.event = 'focus' — フォーム全体のイベント。result = イベント引数の配列 (result[0] が第 1 引数)\nvar push = window._ksLcPush || (window._ksLcPush = function (ev, detail) {\n  var buf = window._ksLcBuf = window._ksLcBuf || [];\n  buf.unshift(new Date().toLocaleTimeString('ja-JP') + '  ' + ev + (detail ? '  ' + detail : ''));\n  if (buf.length > 30) { buf.length = 30; }\n  var box = document.getElementById('lcEventLogBox');\n  if (box) { box.textContent = buf.join('\\n'); }\n});\npush('focus', 'key=' + (result[0] && result[0].key));"
                        }
                      ]
                    },
                    {
                      "name": "on_error",
                      "trigger": {
                        "type": "event",
                        "event": "error"
                      },
                      "actions": [
                        {
                          "name": "log",
                          "type": "customAction",
                          "customAction": "// trigger.event = 'error' — フォーム全体のイベント。result = イベント引数の配列 (result[0] が第 1 引数)\nvar push = window._ksLcPush || (window._ksLcPush = function (ev, detail) {\n  var buf = window._ksLcBuf = window._ksLcBuf || [];\n  buf.unshift(new Date().toLocaleTimeString('ja-JP') + '  ' + ev + (detail ? '  ' + detail : ''));\n  if (buf.length > 30) { buf.length = 30; }\n  var box = document.getElementById('lcEventLogBox');\n  if (box) { box.textContent = buf.join('\\n'); }\n});\npush('error', Array.isArray(result[0]) ? result[0].length + ' 件: ' + result[0].map(function (e) { return e.message; }).join(' / ') : String(result[0]));"
                        }
                      ]
                    },
                    {
                      "name": "on_submit",
                      "trigger": {
                        "type": "event",
                        "event": "submit"
                      },
                      "actions": [
                        {
                          "name": "log",
                          "type": "customAction",
                          "customAction": "// trigger.event = 'submit' — フォーム全体のイベント。result = イベント引数の配列 (result[0] が第 1 引数)\nvar push = window._ksLcPush || (window._ksLcPush = function (ev, detail) {\n  var buf = window._ksLcBuf = window._ksLcBuf || [];\n  buf.unshift(new Date().toLocaleTimeString('ja-JP') + '  ' + ev + (detail ? '  ' + detail : ''));\n  if (buf.length > 30) { buf.length = 30; }\n  var box = document.getElementById('lcEventLogBox');\n  if (box) { box.textContent = buf.join('\\n'); }\n});\npush('submit', '(form.submit() の検証通過。consentFlow の保存/申請/承認はこの後 XPages 側で行われる)');"
                        }
                      ]
                    }
                  ]
                },
                {
                  "type": "htmlelement",
                  "tag": "div",
                  "key": "lcEventLog",
                  "input": false,
                  "tableView": false,
                  "content": "<div style='font-size:11px;color:#555;margin-bottom:4px;'>イベントログ (新しい順・最大 30 件)。直前の <code>type:\"hidden\"</code> 項目 <code>lcEventTap</code> の logic[] に initialized / render / change / blur / focus / error / submit を 1 つずつ登録し、customAction でこの枠へ DOM 直書きしています (項目の値は変えないのでループしません)。<strong>htmlelement / content に logic を載せてはいけません</strong>: これらは <code>trigger.event</code> を i18n 翻訳してから購読するため、日本語 UI では <code>error</code> → 「処理前に以下のエラーを修正してください。」、<code>submit</code> → 「フォームの送信」というイベント名で待ち続け、永久に発火しません (blur / change 等は辞書に無いので偶然動く)。保存 / 申請 / 承認は consentFlow が先に <code>checkValidity()</code> で止めるので <code>error</code> は出ず、検証に通っても <code>form.submit()</code> の完了 (非同期) より先に XPages の送信が始まるので <code>submit</code> も通常は出ません (開発者ツールで <code>myFormio.form.submit()</code> を直接呼ぶと両方確認できます)。</div><div id='lcEventLogBox' class='ks-url' style='white-space:pre;min-height:120px;max-height:260px;overflow:auto;'>(読み込み中…)</div>"
                }
              ]
            },
            {
              "type": "panel",
              "key": "ksBlockInitOnce",
              "title": "🚀 文書を開いたとき 1 回だけ実行する — フォームスクリプト (scriptBlock + cfAPI.form): ページ表示ごと / 新規作成時のみ / バージョン移行",
              "customClass": "ks-block",
              "collapsible": true,
              "collapsed": false,
              "input": false,
              "tableView": false,
              "components": [
                {
                  "type": "htmlelement",
                  "tag": "div",
                  "className": "ks-note",
                  "key": "fsIntro",
                  "input": false,
                  "tableView": false,
                  "content": "Notes の PostOpen / QueryOpen に当たる「開いたとき 1 回だけ」は、<strong>予約キー <code>scriptBlock</code> のコンポーネント</strong> (このパネルの先頭、画面には出ない) に JavaScript を書き、<code>cfAPI.form.*</code> にコールバックを登録します。<strong>ガード・setTimeout・値の復元はコアが面倒を見る</strong>ので、開発者はコールバックの中身だけを書きます。呼び出し順は onNew → migrate → onOpen → onReady、いずれも保存データの適用後です。<br><table class='ks-spec' style='margin-top:8px'><thead><tr><th style='width:26%'>API</th><th>いつ 1 回</th><th>用途</th></tr></thead><tbody><tr><td><code>cfAPI.form.onOpen(fn)</code></td><td>① ページ表示ごと (参照モードでも)。部分更新で Form.io が作り直されても再実行しない</td><td>開くたびの初期化・マスタの 1 回読み (REST)・URL パラメータの読み取り</td></tr><tr><td><code>cfAPI.form.onNew(fn)</code></td><td>② 新規作成時 (<code>cfGlobal.isNewNote</code>)</td><td>作成時刻・作成者・作成時点の所属スナップショット</td></tr><tr><td><code>cfAPI.form.migrate(ver, fn)</code></td><td>③ 未実施の既存文書を編集モードで開いたとき (番号の昇順)。新規文書は実行せず最新番号を刻む</td><td>既存文書のデータ移行・正規化</td></tr><tr><td><code>cfAPI.form.onReady(fn)</code></td><td>Form.io が (再) 生成されるたび</td><td>DOM へのイベント束縛・共有ヘルパ (<code>cfAPI.form.lib</code>) の公開</td></tr></tbody></table><code>ctx</code> = <code>{ form, data, cfGlobal, cfAPI, lib, isNewNote, editable, docid, get(key), set(key, value), component(key) }</code>。<code>ctx.set</code> は未保存警告を出しません。コールバックが Promise を返すとコアは完了を待ちます (自動保存の基準はその後)。"
                },
                {
                  "type": "htmlelement",
                  "tag": "div",
                  "key": "scriptBlock",
                  "label": "フォームスクリプト (scriptBlock)",
                  "input": false,
                  "hidden": true,
                  "hideLabel": true,
                  "tableView": false,
                  "content": "// ===== フォームスクリプト (予約キー scriptBlock) =====\n// このコンポーネントの本文はコアが JavaScript として評価し、画面には表示されません。\n// 登録したコールバックは「保存データの適用後」にコアが 1 回だけ呼びます (順序: onNew → migrate → onOpen → onReady)。\n// ctx: { form, data, cfGlobal, cfAPI, lib, isNewNote, editable, docid, get(key), set(key, value), component(key) }\n// ctx.set は未保存警告 (dirty) を立てません。他の項目は ctx.get / ctx.set で読み書きします。\n\n// ② 新規作成時に 1 回 — 作成時刻・作成者・作成時点の所属のスナップショット (persistent な hidden に保存)\ncfAPI.form.onNew(function (ctx) {\n  ctx.set('fsNewStamp', JSON.stringify({\n    at: new Date().toISOString(),\n    user: cfGlobal.currentUserName || '',\n    sectionCode: cfGlobal.sectionCode || '',\n    sectionName: cfGlobal.sectionName || '',\n    rev: cfGlobal.webFormRevisionNo || ''\n  }));\n});\n\n// ③ 未実施の既存文書を編集モードで開いたとき 1 回 — バージョン番号で管理 (実施済み番号は hidden cfMigrationVersion にコアが保存)\n//    移行内容を変えたら「新しい番号で追加」する (既存番号は書き換えない)。新規文書は実行せず最新番号が刻まれる\ncfAPI.form.migrate(1, function (ctx) {\n  var v = String(ctx.get('fsLegacyField') || '');\n  ctx.set('fsLegacyField', v.replace(/[Ａ-Ｚａ-ｚ０-９]/g, function (ch) { return String.fromCharCode(ch.charCodeAt(0) - 0xFEE0); }));\n});\n\n// ① ページ表示ごとに 1 回 (参照モードでも) — 開くたびの初期化・マスタの 1 回読み。部分更新で Form.io が作り直されても再実行されない\ncfAPI.form.onOpen(function (ctx) {\n  ctx.set('fsOpenRuns', (parseInt(ctx.get('fsOpenRuns'), 10) || 0) + 1);\n  ctx.set('fsOpenAt', new Date().toLocaleTimeString('ja-JP'));\n  if (ctx.isNewNote) { ctx.set('fsAsyncResult', '(新規文書のため REST はスキップ。保存して開き直すと実行されます)'); return; }\n  ctx.set('fsAsyncResult', '取得中...');\n  // Promise (jqXHR) を返すと、コアは完了を待ってから自動保存の基準ハッシュを取る\n  return $.ajax({ url: './restDocJson.xsp/json?srv=masterdb&db=masterdb&unid=' + encodeURIComponent(ctx.docid), method: 'GET', timeout: 30000 })\n    .then(function (doc) {\n      var n = Object.keys(doc || {}).filter(function (k) { return k.charAt(0) !== '@'; }).length;\n      ctx.set('fsAsyncResult', 'Form=' + ((doc && doc.Form) || '?') + ' / 項目数 ' + n + ' / 取得 ' + new Date().toLocaleTimeString('ja-JP'));\n    }, function (xhr, status) {\n      ctx.set('fsAsyncResult', 'エラー: ' + status);\n    });\n});\n\n// Form.io インスタンスが (再) 生成されるたび — DOM へのイベント束縛や共有ヘルパの公開はここ\ncfAPI.form.onReady(function (ctx) {\n  ctx.set('fsReadyRuns', (parseInt(ctx.get('fsReadyRuns'), 10) || 0) + 1);\n  cfAPI.form.lib.zenToHan = function (s) { return String(s || '').replace(/[Ａ-Ｚａ-ｚ０-９]/g, function (ch) { return String.fromCharCode(ch.charCodeAt(0) - 0xFEE0); }); };\n});"
                },
                {
                  "type": "panel",
                  "key": "fsPart1",
                  "title": "① ページ表示ごとに 1 回 — cfAPI.form.onOpen (非同期 REST もここで 1 回だけ)",
                  "customClass": "ks-part",
                  "collapsible": false,
                  "input": false,
                  "tableView": false,
                  "components": [
                    {
                      "type": "columns",
                      "key": "fsCols1",
                      "label": "Columns",
                      "input": false,
                      "tableView": false,
                      "columns": [
                        {
                          "components": [
                            {
                              "type": "textfield",
                              "key": "fsOpenAt",
                              "label": "① onOpen 実行時刻 (保存されません ※設定「persistent:false / Client-only の項目を保存しない」が はい のとき)",
                              "input": true,
                              "tableView": false,
                              "disabled": true,
                              "persistent": false
                            }
                          ],
                          "width": 3,
                          "offset": 0,
                          "push": 0,
                          "pull": 0,
                          "size": "md",
                          "currentWidth": 3
                        },
                        {
                          "components": [
                            {
                              "type": "textarea",
                              "key": "fsAsyncResult",
                              "label": "① 非同期処理の結果 (restDocJson で現在文書を 1 回読む)",
                              "input": true,
                              "tableView": false,
                              "disabled": true,
                              "persistent": false,
                              "rows": 2,
                              "autoExpand": true
                            }
                          ],
                          "width": 6,
                          "offset": 0,
                          "push": 0,
                          "pull": 0,
                          "size": "md",
                          "currentWidth": 6
                        },
                        {
                          "components": [
                            {
                              "type": "number",
                              "key": "fsOpenRuns",
                              "label": "onOpen 実行回数 (保存されます)",
                              "input": true,
                              "tableView": false,
                              "disabled": true,
                              "persistent": true,
                              "defaultValue": 0
                            }
                          ],
                          "width": 3,
                          "offset": 0,
                          "push": 0,
                          "pull": 0,
                          "size": "md",
                          "currentWidth": 3
                        }
                      ]
                    },
                    {
                      "type": "columns",
                      "key": "fsCols2",
                      "label": "Columns",
                      "input": false,
                      "tableView": false,
                      "columns": [
                        {
                          "components": [
                            {
                              "type": "textfield",
                              "key": "fsTypeHere",
                              "label": "ここに何か入力しても ① は再実行されません",
                              "input": true,
                              "tableView": false,
                              "persistent": false,
                              "placeholder": "calculateValue と違い、入力のたびに評価されることがない"
                            }
                          ],
                          "width": 4,
                          "offset": 0,
                          "push": 0,
                          "pull": 0,
                          "size": "md",
                          "currentWidth": 4
                        },
                        {
                          "components": [
                            {
                              "type": "number",
                              "key": "fsReadyRuns",
                              "label": "onReady 実行回数 (保存されます)",
                              "input": true,
                              "tableView": false,
                              "disabled": true,
                              "persistent": true,
                              "defaultValue": 0
                            }
                          ],
                          "width": 2,
                          "offset": 0,
                          "push": 0,
                          "pull": 0,
                          "size": "md",
                          "currentWidth": 2
                        },
                        {
                          "components": [
                            {
                              "type": "button",
                              "key": "btnFsRefresh",
                              "label": "🔁 部分更新だけ (Form.io を作り直す)",
                              "action": "custom",
                              "theme": "secondary",
                              "input": true,
                              "tableView": false,
                              "showValidations": false,
                              "block": true,
                              "customClass": "ks-run-btn",
                              "custom": "var mp = document.querySelector(\"[id$='mainPanel']\"); myFormio.syncFields({ refreshId: mp ? mp.id : undefined });",
                              "description": "onReady は増え、onOpen は増えない (ページ表示ごとに 1 回のまま)"
                            }
                          ],
                          "width": 3,
                          "offset": 0,
                          "push": 0,
                          "pull": 0,
                          "size": "md",
                          "currentWidth": 3
                        },
                        {
                          "components": [
                            {
                              "type": "button",
                              "key": "btnFsRerunPage",
                              "label": "🔄 ページフラグをリセット → 再描画",
                              "action": "custom",
                              "theme": "secondary",
                              "input": true,
                              "tableView": false,
                              "showValidations": false,
                              "block": true,
                              "customClass": "ks-run-btn",
                              "custom": "delete window.__cfFormScript; var mp = document.querySelector(\"[id$='mainPanel']\"); myFormio.syncFields({ refreshId: mp ? mp.id : undefined });",
                              "description": "ページ再読込と同じ状態にしてから作り直す → onOpen がもう一度 1 回走る"
                            }
                          ],
                          "width": 3,
                          "offset": 0,
                          "push": 0,
                          "pull": 0,
                          "size": "md",
                          "currentWidth": 3
                        }
                      ]
                    },
                    {
                      "type": "htmlelement",
                      "tag": "div",
                      "className": "ks-howto",
                      "key": "fsPart1HowTo",
                      "input": false,
                      "tableView": false,
                      "content": "🔧 <strong>流用するには</strong>: scriptBlock の <code>cfAPI.form.onOpen(function (ctx) { … })</code> の中身を差し替える。① 結果を書く先は <code>ctx.set('キー', 値)</code> ② REST を呼ぶなら <code>ctx.isNewNote</code> で分岐 (新規時の <code>ctx.docid</code> は仮 UNID) ③ Promise を <code>return</code> すると完了まで待ってもらえる。<code>window.__xxxDone</code> フラグ・<code>setTimeout(fn, 0)</code>・値の復元用 calculateValue は <strong>不要</strong> (コアが保存データ適用後に 1 回だけ呼ぶため)。"
                    }
                  ]
                },
                {
                  "type": "panel",
                  "key": "fsPart2",
                  "title": "② 新規作成時に 1 回だけ — cfAPI.form.onNew (保存される)",
                  "customClass": "ks-part",
                  "collapsible": false,
                  "input": false,
                  "tableView": false,
                  "components": [
                    {
                      "type": "hidden",
                      "key": "fsNewStamp",
                      "input": true,
                      "persistent": true
                    },
                    {
                      "type": "textfield",
                      "key": "fsNewStampDisp",
                      "label": "② 作成時スナップショット (新規作成時に 1 回だけ生成・以後は保存値を表示)",
                      "input": true,
                      "tableView": false,
                      "disabled": true,
                      "persistent": false,
                      "allowCalculateOverride": false,
                      "calculateValue": "if (!data.fsNewStamp) { value = (typeof cfGlobal === 'undefined') ? '(ビルダープレビュー: cfGlobal 未定義)' : '(値なし — この文書はスナップショット導入前に作成されたか、まだ評価前)'; } else { try { var s = JSON.parse(data.fsNewStamp); value = '作成 ' + s.at + ' / ' + s.user + ' / 所属 ' + s.sectionName + ' (' + s.sectionCode + ') / rev ' + s.rev; } catch (e) { value = data.fsNewStamp; } }"
                    },
                    {
                      "type": "htmlelement",
                      "tag": "div",
                      "className": "ks-howto",
                      "key": "fsPart2HowTo",
                      "input": false,
                      "tableView": false,
                      "content": "🔧 <strong>流用するには</strong>: <code>onNew</code> の中で確定させたい値を <code>ctx.set</code> する (作成時の所属コード・単価表バージョン・採番シードなど)。保存先は <code>persistent:true</code> の hidden。<strong>単一項目の既定値</strong> (今日の日付など) は従来どおりその項目の <code>calculateValue</code> に <code>cfGlobal.isNewNote &amp;&amp; !value</code> ガードで書いてもよい (<code>customDefaultValue</code> は公開時の評価値が焼き込まれるので使わない)。コピー・子文書は <code>isNewNote=false</code> なので対象外。"
                    }
                  ]
                },
                {
                  "type": "panel",
                  "key": "fsPart3",
                  "title": "③ 未実施の文書を開いたとき 1 回 — cfAPI.form.migrate(ver, fn) (既存文書も対象・バージョン管理)",
                  "customClass": "ks-part",
                  "collapsible": false,
                  "input": false,
                  "tableView": false,
                  "components": [
                    {
                      "type": "columns",
                      "key": "fsCols3",
                      "label": "Columns",
                      "input": false,
                      "tableView": false,
                      "columns": [
                        {
                          "components": [
                            {
                              "type": "textfield",
                              "key": "fsLegacyField",
                              "label": "移行対象フィールド (保存されます) — 全角英数字は開いたとき 1 回だけ半角に正規化",
                              "input": true,
                              "tableView": false,
                              "persistent": true,
                              "defaultValue": "ＡＢＣ１２３ｘｙｚ",
                              "description": "編集モードで開いた直後に半角化され、その後に全角を打っても再正規化されない (実施済みバージョンが刻まれるため)"
                            }
                          ],
                          "width": 6,
                          "offset": 0,
                          "push": 0,
                          "pull": 0,
                          "size": "md",
                          "currentWidth": 6
                        },
                        {
                          "components": [
                            {
                              "type": "textfield",
                              "key": "fsVersionDisp",
                              "label": "③ 実施済みバージョン (hidden cfMigrationVersion・保存されます)",
                              "input": true,
                              "tableView": false,
                              "disabled": true,
                              "persistent": false,
                              "allowCalculateOverride": false,
                              "calculateValue": "value = data.cfMigrationVersion ? 'v' + data.cfMigrationVersion + ' 実施済み' : '(未実施 — 編集モードで開くと実行)';"
                            }
                          ],
                          "width": 3,
                          "offset": 0,
                          "push": 0,
                          "pull": 0,
                          "size": "md",
                          "currentWidth": 3
                        },
                        {
                          "components": [
                            {
                              "type": "button",
                              "key": "btnFsResetVersion",
                              "label": "🔄 実施済みを消して再描画",
                              "action": "custom",
                              "theme": "secondary",
                              "input": true,
                              "tableView": false,
                              "showValidations": false,
                              "block": true,
                              "customClass": "ks-run-btn",
                              "custom": "form.getComponent('cfMigrationVersion').setValue(''); form.getComponent('fsLegacyField').setValue('ＡＢＣ１２３ｘｙｚ'); var mp = document.querySelector(\"[id$='mainPanel']\"); myFormio.syncFields({ refreshId: mp ? mp.id : undefined });",
                              "description": "未実施に戻してから Form.io を作り直す → 移行がもう一度 1 回走る"
                            }
                          ],
                          "width": 3,
                          "offset": 0,
                          "push": 0,
                          "pull": 0,
                          "size": "md",
                          "currentWidth": 3
                        }
                      ]
                    },
                    {
                      "type": "htmlelement",
                      "tag": "div",
                      "className": "ks-howto",
                      "key": "fsPart3HowTo",
                      "input": false,
                      "tableView": false,
                      "content": "🔧 <strong>流用するには</strong>: ① 移行の中身を <code>migrate(1, …)</code> に書く ② 移行内容を変えたら <code>migrate(2, …)</code> を<strong>追加</strong>する (v1 → v2 と積み上げ。未実施の文書は昇順に全部走る) ③ 実施済み番号は hidden <code>cfMigrationVersion</code> にコアが保存する (フォームに置く必要なし。<code>CF_cfMigrationVersion</code> として Notes にも残る)。参照モードでは走らない (保存されないため)。リビジョン保存で固定された既存文書は旧 JSON を開くので届かない (通常保存は全文書に届く)。"
                    }
                  ]
                },
                {
                  "type": "htmlelement",
                  "tag": "div",
                  "className": "ks-note",
                  "key": "fsPitfalls",
                  "input": false,
                  "tableView": false,
                  "content": "⚠️ <strong>落とし穴</strong>: (1) <code>key</code> は厳密に <code>scriptBlock</code>、1 フォームに 1 つ。<code>hidden: true</code> を付ける (旧バージョンの consentFlow では無害な no-op になる) (2) スクリプトは <code>cfAPI</code> / <code>cfGlobal</code> / <code>form</code> / <code>$</code> を引数に評価される。ビルダープレビューでは評価されない (<code>cfGlobal</code> 未定義) (3) 例外は握り潰されず <code>console.error('[cfAPI.form] …')</code> と <code>cfAPI.form.errors</code> に残る。<code>migrate</code> は失敗した番号で止まり、印は進まない (4) <code>ctx.set</code> は未保存警告を出さないので、移行結果は利用者が保存したときに確定する。サーバー側で「保存時に 1 回」確実に行いたい処理は <code>customExt*</code> フック (プロコード) の領域 (5) 手書き版 (<code>type:\"hidden\"</code> + <code>calculateValue</code> + window フラグ) は旧バージョンの consentFlow 向けのフォールバックとして引き続き動く。"
                }
              ]
            },
            {
              "type": "hidden",
              "key": "hiddenStamp",
              "input": true,
              "persistent": true
            }
          ]
        },
        {
          "key": "tabCf",
          "label": "🔗 consentFlow 連携",
          "components": [
            {
              "type": "htmlelement",
              "tag": "div",
              "className": "ks-note",
              "key": "introCf",
              "content": "サーバーデータ読出し 3 経路のデモ: <code>cfGlobal</code> (上部パネル参照) / <code>cfAPI</code> (現在文書の任意アイテム・<strong>isNewNote ガード必須</strong>) / <code>restViewJson・restDocJson</code>。下の <strong>View/文書エクスプローラ</strong> に DB パスとビュー名を入力すると、任意のマスタを画面から探索できます。",
              "input": false,
              "tableView": false
            },
            {
              "type": "panel",
              "key": "ksBlockCfApi",
              "title": "📄 cfAPI.getCurrentDoc() — isNewNote ガード付き",
              "customClass": "ks-block",
              "collapsible": true,
              "collapsed": false,
              "input": false,
              "tableView": false,
              "components": [
                {
                  "type": "columns",
                  "key": "cfApiCols",
                  "label": "Columns",
                  "input": false,
                  "tableView": false,
                  "columns": [
                    {
                      "components": [
                        {
                          "type": "textfield",
                          "key": "cfApiFieldName",
                          "label": "読み取る Notes アイテム名",
                          "input": true,
                          "tableView": false,
                          "persistent": false,
                          "defaultValue": "Form",
                          "placeholder": "例: Form / StatusNo / CF_lastName",
                          "description": "現在文書の任意アイテムを cfAPI で読み取ります (接頭語付きの実アイテム名で指定)"
                        }
                      ],
                      "width": 4,
                      "offset": 0,
                      "push": 0,
                      "pull": 0,
                      "size": "md",
                      "currentWidth": 4
                    },
                    {
                      "components": [
                        {
                          "type": "button",
                          "key": "btnCfApiRead",
                          "label": "指定フィールドを読む",
                          "action": "custom",
                          "theme": "primary",
                          "input": true,
                          "tableView": false,
                          "showValidations": false,
                          "custom": "if (typeof cfGlobal === 'undefined') { alert('この機能は consentFlow 実行環境でのみ動作します (ビルダープレビュー不可)'); return; }\nvar fieldName = (data.cfApiFieldName || '').trim();\nif (!fieldName) { alert('読み取る Notes アイテム名を入力してください'); return; }\nif (cfGlobal.isNewNote) { form.getComponent('cfApiResult').setValue('(新規文書のためスキップ — isNewNote ガード。保存後に再実行してください)'); return; }\nvar v = cfAPI.getDb().getCurrentDoc().getItemValueString(fieldName);\nform.getComponent('cfApiResult').setValue(fieldName + ' = ' + ((v === null || v === undefined || v === '') ? '(空または項目なし)' : v));"
                        }
                      ],
                      "width": 3,
                      "offset": 0,
                      "push": 0,
                      "pull": 0,
                      "size": "md",
                      "currentWidth": 3
                    },
                    {
                      "components": [
                        {
                          "type": "textfield",
                          "key": "cfApiResult",
                          "label": "取得結果 (保存されません ※設定「persistent:false / Client-only の項目を保存しない」が はい のとき)",
                          "input": true,
                          "tableView": false,
                          "disabled": true,
                          "persistent": false
                        }
                      ],
                      "width": 5,
                      "offset": 0,
                      "push": 0,
                      "pull": 0,
                      "size": "md",
                      "currentWidth": 5
                    }
                  ]
                }
              ]
            },
            {
              "type": "panel",
              "key": "ksBlockViewExp",
              "title": "🔍 restViewJson — View エクスプローラ ★基礎設定 (このタブの全機能が参照)",
              "customClass": "ks-block",
              "collapsible": true,
              "collapsed": false,
              "input": false,
              "tableView": false,
              "components": [
                {
                  "type": "htmlelement",
                  "tag": "div",
                  "key": "configWarnBanner",
                  "input": false,
                  "tableView": false,
                  "content": "<div style='background:#fff3e0;border:2px solid #fb8c00;border-radius:6px;padding:12px 16px;margin-bottom:15px;font-size:13px;color:#e65100;'><strong>⚠️ まず基礎設定を入力してください</strong> — このタブの <strong>View エクスプローラ / keys= 検索 / ダイアログピッカー / 文書エクスプローラ</strong> はすべて下の「★① DB パス」「★② ビュー名」を参照します。<strong>ビュー名を入力すると各機能が動き出します。</strong></div>",
                  "customConditional": "show = !((data.expViewName || '').trim());"
                },
                {
                  "type": "htmlelement",
                  "tag": "div",
                  "key": "configOkBanner",
                  "input": false,
                  "tableView": false,
                  "refreshOnChange": true,
                  "content": "<div style='background:#e8f5e9;border:2px solid #66bb6a;border-radius:6px;padding:10px 16px;margin-bottom:15px;font-size:13px;color:#2e7d32;'><strong>✅ 基礎設定 OK</strong> — 検索対象: <strong>{{ (data.expDbName || 'masterdb') }}</strong> / <strong>{{ data.expViewName }}</strong> (このタブの全機能がこの View を使用します)</div>",
                  "customConditional": "show = !!((data.expViewName || '').trim());"
                },
                {
                  "type": "htmlelement",
                  "tag": "div",
                  "className": "ks-note",
                  "key": "restNote",
                  "content": "DB パス (空欄/<code>masterdb</code> = wfConfig の masterdb 設定に解決) とビュー名を入力すると、select が <code>{{data.expViewUrl}}</code> の URL 補間 + <code>refreshOn</code> + <code>clearOnRefresh:true</code> で自動再取得します。行を選ぶと<strong>行オブジェクト JSON</strong> と<strong>列プログラム名一覧</strong>が表示されます — 列名が <code>$11</code> のような自動採番になっていたら View 列に「プログラム名」を設定してください。多列キー完全一致は <code>keysEx=v1,v2</code>。",
                  "input": false,
                  "tableView": false
                },
                {
                  "type": "columns",
                  "key": "expCols1",
                  "label": "Columns",
                  "input": false,
                  "tableView": false,
                  "columns": [
                    {
                      "components": [
                        {
                          "type": "textfield",
                          "key": "expDbName",
                          "label": "① DB パス【基礎設定】",
                          "input": true,
                          "tableView": false,
                          "defaultValue": "masterdb",
                          "persistent": false,
                          "placeholder": "masterdb / apps/data/master.nsf",
                          "description": "空欄 or masterdb = wfConfig の masterdb に解決",
                          "customClass": "ks-config-field"
                        }
                      ],
                      "width": 6,
                      "offset": 0,
                      "push": 0,
                      "pull": 0,
                      "size": "md",
                      "currentWidth": 6
                    },
                    {
                      "components": [
                        {
                          "type": "textfield",
                          "key": "expViewName",
                          "label": "② ビュー名【基礎設定】(エイリアス可)",
                          "input": true,
                          "tableView": false,
                          "persistent": false,
                          "placeholder": "例: vYourMaster",
                          "customClass": "ks-config-field"
                        }
                      ],
                      "width": 6,
                      "offset": 0,
                      "push": 0,
                      "pull": 0,
                      "size": "md",
                      "currentWidth": 6
                    }
                  ]
                },
                {
                  "type": "htmlelement",
                  "tag": "div",
                  "key": "expViewUrlDisp",
                  "input": false,
                  "tableView": false,
                  "refreshOnChange": true,
                  "content": "<div class='ks-url'><span class='ks-url-tag'>GET</span>{{ data.expViewUrl || '(★② ビュー名を入力すると select が発行する URL がここに表示されます)' }}</div>"
                },
                {
                  "type": "hidden",
                  "key": "expViewUrl",
                  "input": true,
                  "persistent": false,
                  "calculateValue": "var db = (data.expDbName || '').trim() || 'masterdb';\nvar vw = (data.expViewName || '').trim();\nvalue = vw ? ('./restViewJson.xsp/json?srv=masterdb&db=' + encodeURIComponent(db) + '&vw=' + encodeURIComponent(vw) + '&count=50') : '';",
                  "allowCalculateOverride": false
                },
                {
                  "type": "columns",
                  "key": "expCols2",
                  "label": "Columns",
                  "input": false,
                  "tableView": false,
                  "columns": [
                    {
                      "components": [
                        {
                          "type": "select",
                          "key": "expSelect",
                          "label": "ビュー エントリ (先頭 50 件・保存されません ※設定「persistent:false / Client-only の項目を保存しない」が はい のとき)",
                          "input": true,
                          "tableView": false,
                          "widget": "choicesjs",
                          "dataSrc": "url",
                          "data": {
                            "url": "{{data.expViewUrl}}",
                            "headers": []
                          },
                          "refreshOn": "expViewUrl",
                          "clearOnRefresh": true,
                          "valueProperty": "",
                          "dataType": "object",
                          "idPath": "@unid",
                          "disableLimit": true,
                          "lazyLoad": true,
                          "searchEnabled": true,
                          "persistent": false,
                          "defaultValue": null,
                          "template": "<span>{{ item['@unid'] }}</span>",
                          "description": "行オブジェクト保持の定石: dataType:\"object\" + valueProperty:\"\" + idPath:\"@unid\"。URL 変更時の再取得には clearOnRefresh:true をセットで指定 (古い選択値の残留防止)"
                        }
                      ],
                      "width": 5,
                      "offset": 0,
                      "push": 0,
                      "pull": 0,
                      "size": "md",
                      "currentWidth": 5
                    },
                    {
                      "components": [
                        {
                          "type": "textarea",
                          "key": "expRowJson",
                          "label": "選択行の JSON (dataType:\"object\" が保存する形)",
                          "rows": 5,
                          "input": true,
                          "tableView": false,
                          "disabled": true,
                          "persistent": false,
                          "allowCalculateOverride": false,
                          "calculateValue": "value = (data.expSelect && typeof data.expSelect === 'object') ? JSON.stringify(data.expSelect, null, 1) : '';"
                        }
                      ],
                      "width": 7,
                      "offset": 0,
                      "push": 0,
                      "pull": 0,
                      "size": "md",
                      "currentWidth": 7
                    }
                  ]
                },
                {
                  "type": "textfield",
                  "key": "expColumns",
                  "label": "行オブジェクトのキー一覧 (= 列プログラム名 + @メタデータ)",
                  "input": true,
                  "tableView": false,
                  "disabled": true,
                  "persistent": false,
                  "allowCalculateOverride": false,
                  "calculateValue": "var row = data.expSelect; value = (row && typeof row === 'object') ? Object.keys(row).join(', ') : '';"
                },
                {
                  "type": "columns",
                  "key": "lookupCols",
                  "label": "Columns",
                  "input": false,
                  "tableView": false,
                  "columns": [
                    {
                      "components": [
                        {
                          "type": "textfield",
                          "key": "lookupKey",
                          "label": "検索キー",
                          "input": true,
                          "tableView": false,
                          "placeholder": "マスタのキー値",
                          "description": "上で指定した View に対する keys= 完全一致検索"
                        }
                      ],
                      "width": 4,
                      "offset": 0,
                      "push": 0,
                      "pull": 0,
                      "size": "md",
                      "currentWidth": 4
                    },
                    {
                      "components": [
                        {
                          "type": "button",
                          "key": "btnLookup",
                          "label": "🔍 View 検索",
                          "action": "custom",
                          "theme": "primary",
                          "input": true,
                          "tableView": false,
                          "showValidations": false,
                          "custom": "XSP.allowSubmit();  // XPages サブミットガード解除 — async 操作前に必須\nif (typeof cfGlobal === 'undefined') { alert('consentFlow 実行環境でのみ動作します'); return; }\nvar db = (data.expDbName || '').trim() || 'masterdb';\nvar vw = (data.expViewName || '').trim();\nif (!vw) { alert('上の View エクスプローラで DB パスとビュー名を入力してください'); return; }\nvar url = './restViewJson.xsp/json?srv=masterdb&db=' + encodeURIComponent(db) + '&vw=' + encodeURIComponent(vw) + '&keysexactmatch=true&count=5&keys=' + encodeURIComponent(data.lookupKey || '');\n// 発行 URL を UI に表示\nvar _ud = form.getComponent('lookupUrl');\nif (_ud) { _ud.setValue(url); }\n// clear-then-set: 応答前に従属フィールドをクリア (古い値の残留防止)\nform.getComponent('lookupResult').setValue('検索中...');\n$.ajax({ async: true, url: url, method: 'GET', timeout: 30000 })\n.done(function (res) {\n  if (!res || res.length === 0) { form.getComponent('lookupResult').setValue('(該当なし)'); return; }\n  form.getComponent('lookupResult').setValue(JSON.stringify(res[0], null, 1).slice(0, 800));\n}).fail(function (xhr, status) {\n  form.getComponent('lookupResult').setValue('エラー: ' + status + ' — DB パス / ビュー名を確認してください');\n});"
                        }
                      ],
                      "width": 3,
                      "offset": 0,
                      "push": 0,
                      "pull": 0,
                      "size": "md",
                      "currentWidth": 3
                    },
                    {
                      "components": [
                        {
                          "type": "textarea",
                          "key": "lookupResult",
                          "label": "検索結果 (先頭 1 件・保存されません ※設定「persistent:false / Client-only の項目を保存しない」が はい のとき)",
                          "rows": 4,
                          "input": true,
                          "tableView": false,
                          "disabled": true,
                          "persistent": false
                        }
                      ],
                      "width": 5,
                      "offset": 0,
                      "push": 0,
                      "pull": 0,
                      "size": "md",
                      "currentWidth": 5
                    }
                  ]
                },
                {
                  "type": "htmlelement",
                  "tag": "div",
                  "key": "lookupUrlDisp",
                  "input": false,
                  "tableView": false,
                  "refreshOnChange": true,
                  "content": "<div class='ks-url'><span class='ks-url-tag'>GET</span>{{ data.lookupUrl || '(View 検索を実行すると発行 URL がここに表示されます)' }}</div>"
                },
                {
                  "type": "hidden",
                  "key": "lookupUrl",
                  "input": true,
                  "persistent": false
                }
              ]
            },
            {
              "type": "panel",
              "key": "ksBlockRestParts",
              "title": "🧩 restViewJson 実践パーツ — 少しの書き換えでそのまま流用できる 2 部品 (+下の文書ピッカー)",
              "customClass": "ks-block",
              "collapsible": true,
              "collapsed": false,
              "input": false,
              "tableView": false,
              "components": [
                {
                  "type": "htmlelement",
                  "tag": "div",
                  "className": "ks-note",
                  "key": "ptIntro",
                  "input": false,
                  "tableView": false,
                  "content": "ここからは <code>restViewJson.xsp</code> を<strong>業務フォームの部品としてどう使うか</strong>の実装例です。いずれも ★基礎設定 (上の View エクスプローラの DB パス / ビュー名) を参照して<strong>動く状態</strong>で入っているので、お客様のマスタビューを指定してそのまま触ってみてください。各部品の下に「🔧 流用するには」— <strong>書き換える箇所は 3 つだけ</strong> — を示しています。実装済みの部品は 3 種: <strong>① 入力しながら候補が出る Select (前方一致)</strong> / <strong>② 一覧を N 件ずつページ送り (start/count + 並び替え)</strong> / <strong>③ ダイアログで検索 → 候補 DataGrid → 選択で親へ反映</strong> (この下のパネル・検索モード切替付き)。"
                },
                {
                  "type": "panel",
                  "key": "ptPart1",
                  "title": "① コード/名称を打つと前方一致で候補が出る Select → 名称・UNID を自動転記 → ビューに無い項目は restDocJson で補完",
                  "customClass": "ks-part",
                  "collapsible": false,
                  "input": false,
                  "tableView": false,
                  "components": [
                    {
                      "type": "htmlelement",
                      "tag": "div",
                      "className": "ks-note",
                      "key": "ptPart1Note",
                      "input": false,
                      "tableView": false,
                      "content": "<strong>使いどころ</strong>: 取引先コード・型番・社員番号など「先頭から打てば絞れる」マスタ入力。Select に打った文字が <code>&amp;keys=&lt;入力&gt;</code> として送られ (<code>searchField:\"keys\"</code>)、第 1 ソート列の<strong>前方一致</strong>で最大 30 件の候補が返ります。行を選ぶと <code>dataType:\"object\"</code> で行オブジェクトが保持され、下のフィールドが <code>calculateValue</code> で自動転記されます (REST 追加呼び出しなし)。"
                    },
                    {
                      "type": "columns",
                      "key": "ptCols1",
                      "label": "Columns",
                      "input": false,
                      "tableView": false,
                      "columns": [
                        {
                          "components": [
                            {
                              "type": "select",
                              "key": "ptSelect",
                              "label": "候補を検索して選択 (1 文字以上入力 → 前方一致 30 件)",
                              "input": true,
                              "tableView": false,
                              "widget": "choicesjs",
                              "dataSrc": "url",
                              "data": {
                                "url": "{{data.ptSelectUrl}}",
                                "headers": []
                              },
                              "refreshOn": "ptSelectUrl",
                              "clearOnRefresh": true,
                              "valueProperty": "",
                              "dataType": "object",
                              "idPath": "@unid",
                              "disableLimit": true,
                              "lazyLoad": true,
                              "searchEnabled": true,
                              "searchField": "keys",
                              "minSearch": 1,
                              "searchDebounce": 0.3,
                              "persistent": false,
                              "defaultValue": null,
                              "template": "<span>{{ (function(){ var ks = Object.keys(item).filter(function(k){ return k.charAt(0) !== '@'; }).slice(0, 3); return ks.map(function(k){ return item[k]; }).filter(function(v){ return v !== undefined && v !== null && v !== ''; }).join(' / ') || item['@unid']; })() }}</span>",
                              "placeholder": "ここに入力すると候補が出ます",
                              "description": "searchField:\"keys\" = 入力文字を &keys= に載せて前方一致検索。候補表示は先頭 3 列を「/」区切りで自動整形"
                            }
                          ],
                          "width": 6,
                          "offset": 0,
                          "push": 0,
                          "pull": 0,
                          "size": "md",
                          "currentWidth": 6
                        },
                        {
                          "components": [
                            {
                              "type": "textfield",
                              "key": "ptNameColumn",
                              "label": "転記する列のプログラム名 (空 = 先頭列)",
                              "input": true,
                              "tableView": false,
                              "persistent": false,
                              "customClass": "ks-config-field",
                              "placeholder": "例: supplierName",
                              "description": "選択行のどの列を「名称」欄に転記するか"
                            }
                          ],
                          "width": 6,
                          "offset": 0,
                          "push": 0,
                          "pull": 0,
                          "size": "md",
                          "currentWidth": 6
                        }
                      ]
                    },
                    {
                      "type": "hidden",
                      "key": "ptSelectUrl",
                      "input": true,
                      "persistent": false,
                      "allowCalculateOverride": false,
                      "calculateValue": "var db = (data.expDbName || '').trim() || 'masterdb';\nvar vw = (data.expViewName || '').trim();\nvar base = './restViewJson.xsp/json?srv=masterdb&db=' + encodeURIComponent(db) + '&vw=' + encodeURIComponent(vw);\nvalue = vw ? (base + '&count=30') : '';"
                    },
                    {
                      "type": "htmlelement",
                      "tag": "div",
                      "key": "ptSelectUrlDisp",
                      "input": false,
                      "tableView": false,
                      "refreshOnChange": true,
                      "content": "<div class='ks-url'><span class='ks-url-tag'>GET</span>{{ data.ptSelectUrl || '(★② ビュー名を入力すると Select が使う URL がここに出ます — 入力文字は &keys= として末尾に付きます)' }}</div>"
                    },
                    {
                      "type": "columns",
                      "key": "ptCols2",
                      "label": "Columns",
                      "input": false,
                      "tableView": false,
                      "columns": [
                        {
                          "components": [
                            {
                              "type": "textfield",
                              "key": "ptPickedName",
                              "label": "転記された名称 (calculateValue・保存されません ※設定「persistent:false / Client-only の項目を保存しない」が はい のとき)",
                              "input": true,
                              "tableView": false,
                              "persistent": false,
                              "disabled": true,
                              "allowCalculateOverride": false,
                              "calculateValue": "var row = data.ptSelect;\nif (row && typeof row === 'object') {\n  var col = (data.ptNameColumn || '').trim();\n  if (!col) { var ks = Object.keys(row).filter(function (k) { return k.charAt(0) !== '@'; }); col = ks[0] || ''; }\n  value = (col && row[col] !== undefined && row[col] !== null) ? String(row[col]) : '';\n} else { value = ''; }"
                            }
                          ],
                          "width": 6,
                          "offset": 0,
                          "push": 0,
                          "pull": 0,
                          "size": "md",
                          "currentWidth": 6
                        },
                        {
                          "components": [
                            {
                              "type": "textfield",
                              "key": "ptPickedUnid",
                              "label": "転記された UNID (実運用ではここを persistent:true の hidden にして保存)",
                              "input": true,
                              "tableView": false,
                              "persistent": false,
                              "disabled": true,
                              "allowCalculateOverride": false,
                              "calculateValue": "var row = data.ptSelect;\nvalue = (row && typeof row === 'object' && row['@unid']) ? row['@unid'] : '';"
                            }
                          ],
                          "width": 6,
                          "offset": 0,
                          "push": 0,
                          "pull": 0,
                          "size": "md",
                          "currentWidth": 6
                        }
                      ]
                    },
                    {
                      "type": "columns",
                      "key": "ptCols3",
                      "label": "Columns",
                      "input": false,
                      "tableView": false,
                      "columns": [
                        {
                          "components": [
                            {
                              "type": "textfield",
                              "key": "ptExtraItem",
                              "label": "ビューに無い項目名 (Notes アイテム名) を補完",
                              "input": true,
                              "tableView": false,
                              "persistent": false,
                              "customClass": "ks-config-field",
                              "placeholder": "例: Remarks",
                              "description": "上の Select で選んだ文書 (「転記された UNID」の文書) に対して restDocJson を 1 回だけ引く。項目名は大小文字を区別"
                            }
                          ],
                          "width": 4,
                          "offset": 0,
                          "push": 0,
                          "pull": 0,
                          "size": "md",
                          "currentWidth": 4
                        },
                        {
                          "components": [
                            {
                              "type": "button",
                              "key": "btnPtExtra",
                              "label": "📄 選択文書からこの項目を取得",
                              "action": "custom",
                              "theme": "secondary",
                              "input": true,
                              "tableView": false,
                              "showValidations": false,
                              "block": true,
                              "customClass": "ks-run-btn",
                              "custom": "XSP.allowSubmit();  // async 操作前に必須\nif (typeof cfGlobal === 'undefined') { alert('consentFlow 実行環境でのみ動作します'); return; }\nvar unid = (data.ptPickedUnid || '').trim();\nvar item = (data.ptExtraItem || '').trim();\nif (!unid) { alert('先に上の Select で文書を選択してください'); return; }\nif (!item) { alert('補完したい項目名 (Notes アイテム名) を入力してください'); return; }\nvar db = (data.expDbName || '').trim() || 'masterdb';\nvar url = './restDocJson.xsp/json?srv=masterdb&db=' + encodeURIComponent(db) + '&unid=' + encodeURIComponent(unid);\nvar ud = form.getComponent('ptExtraUrl'); if (ud) { ud.setValue(url); }\nvar out = form.getComponent('ptExtraValue');\nout.setValue('取得中...');   // clear-then-set\n$.ajax({ async: true, url: url, method: 'GET', timeout: 30000 })\n.done(function (doc) {\n  if (!doc || !doc['@unid']) { out.setValue('(文書が見つかりません: unid=' + unid + ')'); return; }\n  var v = doc[item];\n  if (v === undefined) {\n    // 大小文字違いの候補と、この文書に実在する項目名を提示して自己診断できるようにする\n    var names = Object.keys(doc).filter(function (k) { return k.charAt(0) !== '@'; });\n    var ci = names.filter(function (k) { return k.toLowerCase() === item.toLowerCase(); });\n    out.setValue('項目 \"' + item + '\" は文書 ' + unid + ' (Form=' + (doc.Form || doc['@form'] || '?') + ') に存在しません。'\n      + (ci.length ? ' 大小文字違いの候補: ' + ci.join(', ') + ' (項目名は大小文字を区別します)' : '')\n      + '\\nこの文書の項目 (' + names.length + '): ' + names.join(', '));\n    return;\n  }\n  var text = Array.isArray(v) ? v.join(', ') : (v && typeof v === 'object' ? JSON.stringify(v) : String(v));\n  out.setValue(item + ' = ' + text + '\\n(unid=' + unid + ')');\n})\n.fail(function (xhr, status) { out.setValue('エラー: ' + status + ' (unid=' + unid + ')'); });"
                            }
                          ],
                          "width": 3,
                          "offset": 0,
                          "push": 0,
                          "pull": 0,
                          "size": "md",
                          "currentWidth": 3
                        },
                        {
                          "components": [
                            {
                              "type": "textarea",
                              "key": "ptExtraValue",
                              "label": "補完された値 (保存されません ※設定「persistent:false / Client-only の項目を保存しない」が はい のとき)",
                              "rows": 2,
                              "autoExpand": true,
                              "input": true,
                              "tableView": false,
                              "persistent": false,
                              "disabled": true,
                              "description": "見つからない場合は、問い合わせた UNID とその文書に実在する項目名一覧を表示します"
                            }
                          ],
                          "width": 5,
                          "offset": 0,
                          "push": 0,
                          "pull": 0,
                          "size": "md",
                          "currentWidth": 5
                        }
                      ]
                    },
                    {
                      "type": "hidden",
                      "key": "ptExtraUrl",
                      "input": true,
                      "persistent": false
                    },
                    {
                      "type": "htmlelement",
                      "tag": "div",
                      "key": "ptExtraUrlDisp",
                      "input": false,
                      "tableView": false,
                      "refreshOnChange": true,
                      "content": "<div class='ks-url'><span class='ks-url-tag'>GET</span>{{ data.ptExtraUrl || '(取得を実行すると restDocJson の URL がここに出ます)' }}</div>"
                    },
                    {
                      "type": "htmlelement",
                      "tag": "div",
                      "className": "ks-howto",
                      "key": "ptPart1HowTo",
                      "input": false,
                      "tableView": false,
                      "content": "🔧 <strong>流用するには (3 箇所)</strong>: ① <code>ptSelectUrl</code> の calculateValue で <code>vw=</code> をお客様のビュー名に固定 (DB は <code>masterdb</code> かプレースホルダー名) ② <code>ptNameColumn</code> を固定値にして転記列を決める (Select の <code>template</code> も列名指定にすると表示が安定) ③ 転記先 <code>ptPickedName</code> / <code>ptPickedUnid</code> のキーを業務フィールド名に変え、保存したいものは <code>persistent:true</code> に。Select 自体は <code>persistent:false</code> のまま、再表示は保存済み UNID から <code>customDefaultValue</code> で復元するのが定石 (skill server-data.md 参照)。<br>💡 <code>keys=</code> は<strong>前方一致</strong>なので「途中の文字」では当たりません。名称の途中一致で探したい業務は ③ の文書ピッカー (全文検索モード) を使ってください。"
                    }
                  ]
                },
                {
                  "type": "panel",
                  "key": "ptPart2",
                  "title": "② 一覧を N 件ずつページ送り (start / count) — 並び替え付き",
                  "customClass": "ks-part",
                  "collapsible": false,
                  "input": false,
                  "tableView": false,
                  "components": [
                    {
                      "type": "htmlelement",
                      "tag": "div",
                      "className": "ks-note",
                      "key": "ptPart2Note",
                      "input": false,
                      "tableView": false,
                      "content": "<strong>使いどころ</strong>: 数百件以上あるマスタや履歴を、フォーム内の DataGrid に <strong>N 件ずつページ送り</strong>で表示する。<code>count=N</code> で 1 ページ分だけ取り、<code>start</code> を N ずつ進める／戻す (グリッドはそのページの行に置き換え)。<code>count</code> を省略すると <strong>10 件で打ち切られる</strong>ので必ず指定。<code>sortcolumn=&lt;列プログラム名&gt;&amp;sortorder=descending</code> で並び替え。<strong>列に「クリックでソート」(指定する方向) が設定されていないと、サーバーはエラーにせず既定順のまま返す</strong>ので、この部品は返った行の並びを見て「並び替え未適用」の警告を出します。"
                    },
                    {
                      "type": "columns",
                      "key": "pgCols1",
                      "label": "Columns",
                      "input": false,
                      "tableView": false,
                      "columns": [
                        {
                          "components": [
                            {
                              "type": "number",
                              "key": "pgCount",
                              "label": "1 回に読む件数 (count)",
                              "input": true,
                              "tableView": false,
                              "persistent": false,
                              "defaultValue": 20,
                              "customClass": "ks-config-field"
                            }
                          ],
                          "width": 2,
                          "offset": 0,
                          "push": 0,
                          "pull": 0,
                          "size": "md",
                          "currentWidth": 2
                        },
                        {
                          "components": [
                            {
                              "type": "textfield",
                              "key": "pgSortColumn",
                              "label": "並び替え列のプログラム名 (任意)",
                              "input": true,
                              "tableView": false,
                              "persistent": false,
                              "customClass": "ks-config-field",
                              "placeholder": "例: ApplicationNoEx"
                            }
                          ],
                          "width": 3,
                          "offset": 0,
                          "push": 0,
                          "pull": 0,
                          "size": "md",
                          "currentWidth": 3
                        },
                        {
                          "components": [
                            {
                              "type": "checkbox",
                              "key": "pgSortDesc",
                              "label": "降順 (descending)",
                              "input": true,
                              "tableView": false,
                              "persistent": false,
                              "customClass": "ks-run-btn"
                            }
                          ],
                          "width": 2,
                          "offset": 0,
                          "push": 0,
                          "pull": 0,
                          "size": "md",
                          "currentWidth": 2
                        },
                        {
                          "components": [
                            {
                              "type": "button",
                              "key": "btnPgFirst",
                              "label": "⏮ 先頭ページ",
                              "action": "custom",
                              "theme": "primary",
                              "input": true,
                              "tableView": false,
                              "showValidations": false,
                              "block": true,
                              "customClass": "ks-run-btn",
                              "custom": "XSP.allowSubmit();  // async 操作前に必須\nif (typeof cfGlobal === 'undefined') { alert('consentFlow 実行環境でのみ動作します'); return; }\nvar db = (data.expDbName || '').trim() || 'masterdb';\nvar vw = (data.expViewName || '').trim();\nvar base = './restViewJson.xsp/json?srv=masterdb&db=' + encodeURIComponent(db) + '&vw=' + encodeURIComponent(vw);\nif (!vw) { alert('View エクスプローラの ★① DB パス / ★② ビュー名 を先に入力してください'); return; }\n// ページ送り: グリッドは「今のページの N 行」だけに置き換える (追記しない)\nvar cnt = parseInt(data.pgCount, 10); if (!(cnt > 0)) { cnt = 20; } if (cnt > 200) { cnt = 200; }\nvar cur = parseInt(data.pgStart, 10) || 0;                 // 現在表示中ページの先頭位置\nvar start = 0;\nif (component.key === 'btnPgNext') { start = cur + cnt; }\nelse if (component.key === 'btnPgPrev') { start = Math.max(0, cur - cnt); }\nvar url = base + '&start=' + start + '&count=' + cnt;      // count=N 件だけ、start=N×ページ で取得\nvar sc = (data.pgSortColumn || '').trim();\nif (sc) { url += '&sortcolumn=' + encodeURIComponent(sc) + '&sortorder=' + (data.pgSortDesc ? 'descending' : 'ascending'); }\nvar ud = form.getComponent('pgUrl'); if (ud) { ud.setValue(url); }\nvar grid = form.getComponent('pgGrid');\nvar st = form.getComponent('pgStatus');\nst.setValue('読み込み中...');\n$.ajax({ async: true, url: url, method: 'GET', timeout: 30000 })\n.done(function (res) {\n  var rows = (res || []).filter(function (r) { return r && r['@category'] !== true; });\n  var colNames = rows.length ? Object.keys(rows[0]).filter(function (k) { return k.charAt(0) !== '@'; }).slice(0, 3) : [];\n  var toText = function (v) { return (v === undefined || v === null) ? '' : String(v); };\n  var pageRows = rows.map(function (r) { return { unid: toText(r['@unid']), c1: colNames[0] ? toText(r[colNames[0]]) : '', c2: colNames[1] ? toText(r[colNames[1]]) : '', c3: colNames[2] ? toText(r[colNames[2]]) : '' }; });\n  grid.setValue(pageRows);                                  // 置き換え (clear-then-set)\n  form.getComponent('pgStart').setValue(String(start));\n  var more = rows.length >= cnt;\n  form.getComponent('pgHasMore').setValue(more ? '1' : '');\n  form.getComponent('pgHasPrev').setValue(start > 0 ? '1' : '');\n  var page = Math.floor(start / cnt) + 1;\n  // 並び替えが本当に適用されたかの簡易判定 — サーバーはクリックソート不可の列を指定されても\n  // エラーにせず既定順で返す (ExtLib が resortView の例外を握り潰す) ため、行の単調性で検知する\n  var sortNote = '';\n  if (sc && rows.length > 1) {\n    var vals = rows.map(function (r) { return (r[sc] === undefined || r[sc] === null) ? '' : String(r[sc]); });\n    var hasCol = rows.some(function (r) { return r.hasOwnProperty(sc); });\n    var ok = true;\n    for (var i = 1; i < vals.length; i++) { var c = vals[i - 1].localeCompare(vals[i], undefined, { sensitivity: 'base' }); if (data.pgSortDesc ? c < 0 : c > 0) { ok = false; break; } }\n    if (!hasCol) { sortNote = ' / ⚠ 列 \"' + sc + '\" が行 JSON に無い (プログラム名を確認)'; }\n    else if (!ok) { sortNote = ' / ⚠ 並び替え未適用 — 列 \"' + sc + '\" に「クリックでソート」(' + (data.pgSortDesc ? '降順' : '昇順') + ') が無いと既定順で返ります'; }\n    else { sortNote = ' / 並び替え適用 (' + (data.pgSortDesc ? '降順' : '昇順') + ')'; }\n  }\n  st.setValue('ページ ' + page + ' (start=' + start + ', count=' + cnt + ') — ' + rows.length + ' 件表示' + (more ? ' / 続きあり' : ' / 末尾') + sortNote + (colNames.length ? ' / 列: ' + colNames.join(', ') : ''));\n})\n.fail(function (xhr, status) { st.setValue('エラー: ' + status + ' — DB パス / ビュー名 / 並び替え列 (クリックソート可能列か) を確認'); });"
                            }
                          ],
                          "width": 2,
                          "offset": 0,
                          "push": 0,
                          "pull": 0,
                          "size": "md",
                          "currentWidth": 2
                        },
                        {
                          "components": [
                            {
                              "type": "button",
                              "key": "btnPgPrev",
                              "label": "◀ 前の N 件",
                              "action": "custom",
                              "theme": "secondary",
                              "input": true,
                              "tableView": false,
                              "showValidations": false,
                              "block": true,
                              "customClass": "ks-run-btn",
                              "custom": "XSP.allowSubmit();  // async 操作前に必須\nif (typeof cfGlobal === 'undefined') { alert('consentFlow 実行環境でのみ動作します'); return; }\nvar db = (data.expDbName || '').trim() || 'masterdb';\nvar vw = (data.expViewName || '').trim();\nvar base = './restViewJson.xsp/json?srv=masterdb&db=' + encodeURIComponent(db) + '&vw=' + encodeURIComponent(vw);\nif (!vw) { alert('View エクスプローラの ★① DB パス / ★② ビュー名 を先に入力してください'); return; }\n// ページ送り: グリッドは「今のページの N 行」だけに置き換える (追記しない)\nvar cnt = parseInt(data.pgCount, 10); if (!(cnt > 0)) { cnt = 20; } if (cnt > 200) { cnt = 200; }\nvar cur = parseInt(data.pgStart, 10) || 0;                 // 現在表示中ページの先頭位置\nvar start = 0;\nif (component.key === 'btnPgNext') { start = cur + cnt; }\nelse if (component.key === 'btnPgPrev') { start = Math.max(0, cur - cnt); }\nvar url = base + '&start=' + start + '&count=' + cnt;      // count=N 件だけ、start=N×ページ で取得\nvar sc = (data.pgSortColumn || '').trim();\nif (sc) { url += '&sortcolumn=' + encodeURIComponent(sc) + '&sortorder=' + (data.pgSortDesc ? 'descending' : 'ascending'); }\nvar ud = form.getComponent('pgUrl'); if (ud) { ud.setValue(url); }\nvar grid = form.getComponent('pgGrid');\nvar st = form.getComponent('pgStatus');\nst.setValue('読み込み中...');\n$.ajax({ async: true, url: url, method: 'GET', timeout: 30000 })\n.done(function (res) {\n  var rows = (res || []).filter(function (r) { return r && r['@category'] !== true; });\n  var colNames = rows.length ? Object.keys(rows[0]).filter(function (k) { return k.charAt(0) !== '@'; }).slice(0, 3) : [];\n  var toText = function (v) { return (v === undefined || v === null) ? '' : String(v); };\n  var pageRows = rows.map(function (r) { return { unid: toText(r['@unid']), c1: colNames[0] ? toText(r[colNames[0]]) : '', c2: colNames[1] ? toText(r[colNames[1]]) : '', c3: colNames[2] ? toText(r[colNames[2]]) : '' }; });\n  grid.setValue(pageRows);                                  // 置き換え (clear-then-set)\n  form.getComponent('pgStart').setValue(String(start));\n  var more = rows.length >= cnt;\n  form.getComponent('pgHasMore').setValue(more ? '1' : '');\n  form.getComponent('pgHasPrev').setValue(start > 0 ? '1' : '');\n  var page = Math.floor(start / cnt) + 1;\n  // 並び替えが本当に適用されたかの簡易判定 — サーバーはクリックソート不可の列を指定されても\n  // エラーにせず既定順で返す (ExtLib が resortView の例外を握り潰す) ため、行の単調性で検知する\n  var sortNote = '';\n  if (sc && rows.length > 1) {\n    var vals = rows.map(function (r) { return (r[sc] === undefined || r[sc] === null) ? '' : String(r[sc]); });\n    var hasCol = rows.some(function (r) { return r.hasOwnProperty(sc); });\n    var ok = true;\n    for (var i = 1; i < vals.length; i++) { var c = vals[i - 1].localeCompare(vals[i], undefined, { sensitivity: 'base' }); if (data.pgSortDesc ? c < 0 : c > 0) { ok = false; break; } }\n    if (!hasCol) { sortNote = ' / ⚠ 列 \"' + sc + '\" が行 JSON に無い (プログラム名を確認)'; }\n    else if (!ok) { sortNote = ' / ⚠ 並び替え未適用 — 列 \"' + sc + '\" に「クリックでソート」(' + (data.pgSortDesc ? '降順' : '昇順') + ') が無いと既定順で返ります'; }\n    else { sortNote = ' / 並び替え適用 (' + (data.pgSortDesc ? '降順' : '昇順') + ')'; }\n  }\n  st.setValue('ページ ' + page + ' (start=' + start + ', count=' + cnt + ') — ' + rows.length + ' 件表示' + (more ? ' / 続きあり' : ' / 末尾') + sortNote + (colNames.length ? ' / 列: ' + colNames.join(', ') : ''));\n})\n.fail(function (xhr, status) { st.setValue('エラー: ' + status + ' — DB パス / ビュー名 / 並び替え列 (クリックソート可能列か) を確認'); });",
                              "customConditional": "show = data.pgHasPrev === '1';"
                            }
                          ],
                          "width": 1,
                          "offset": 0,
                          "push": 0,
                          "pull": 0,
                          "size": "md",
                          "currentWidth": 1
                        },
                        {
                          "components": [
                            {
                              "type": "button",
                              "key": "btnPgNext",
                              "label": "次の N 件 ▶",
                              "action": "custom",
                              "theme": "secondary",
                              "input": true,
                              "tableView": false,
                              "showValidations": false,
                              "block": true,
                              "customClass": "ks-run-btn",
                              "custom": "XSP.allowSubmit();  // async 操作前に必須\nif (typeof cfGlobal === 'undefined') { alert('consentFlow 実行環境でのみ動作します'); return; }\nvar db = (data.expDbName || '').trim() || 'masterdb';\nvar vw = (data.expViewName || '').trim();\nvar base = './restViewJson.xsp/json?srv=masterdb&db=' + encodeURIComponent(db) + '&vw=' + encodeURIComponent(vw);\nif (!vw) { alert('View エクスプローラの ★① DB パス / ★② ビュー名 を先に入力してください'); return; }\n// ページ送り: グリッドは「今のページの N 行」だけに置き換える (追記しない)\nvar cnt = parseInt(data.pgCount, 10); if (!(cnt > 0)) { cnt = 20; } if (cnt > 200) { cnt = 200; }\nvar cur = parseInt(data.pgStart, 10) || 0;                 // 現在表示中ページの先頭位置\nvar start = 0;\nif (component.key === 'btnPgNext') { start = cur + cnt; }\nelse if (component.key === 'btnPgPrev') { start = Math.max(0, cur - cnt); }\nvar url = base + '&start=' + start + '&count=' + cnt;      // count=N 件だけ、start=N×ページ で取得\nvar sc = (data.pgSortColumn || '').trim();\nif (sc) { url += '&sortcolumn=' + encodeURIComponent(sc) + '&sortorder=' + (data.pgSortDesc ? 'descending' : 'ascending'); }\nvar ud = form.getComponent('pgUrl'); if (ud) { ud.setValue(url); }\nvar grid = form.getComponent('pgGrid');\nvar st = form.getComponent('pgStatus');\nst.setValue('読み込み中...');\n$.ajax({ async: true, url: url, method: 'GET', timeout: 30000 })\n.done(function (res) {\n  var rows = (res || []).filter(function (r) { return r && r['@category'] !== true; });\n  var colNames = rows.length ? Object.keys(rows[0]).filter(function (k) { return k.charAt(0) !== '@'; }).slice(0, 3) : [];\n  var toText = function (v) { return (v === undefined || v === null) ? '' : String(v); };\n  var pageRows = rows.map(function (r) { return { unid: toText(r['@unid']), c1: colNames[0] ? toText(r[colNames[0]]) : '', c2: colNames[1] ? toText(r[colNames[1]]) : '', c3: colNames[2] ? toText(r[colNames[2]]) : '' }; });\n  grid.setValue(pageRows);                                  // 置き換え (clear-then-set)\n  form.getComponent('pgStart').setValue(String(start));\n  var more = rows.length >= cnt;\n  form.getComponent('pgHasMore').setValue(more ? '1' : '');\n  form.getComponent('pgHasPrev').setValue(start > 0 ? '1' : '');\n  var page = Math.floor(start / cnt) + 1;\n  // 並び替えが本当に適用されたかの簡易判定 — サーバーはクリックソート不可の列を指定されても\n  // エラーにせず既定順で返す (ExtLib が resortView の例外を握り潰す) ため、行の単調性で検知する\n  var sortNote = '';\n  if (sc && rows.length > 1) {\n    var vals = rows.map(function (r) { return (r[sc] === undefined || r[sc] === null) ? '' : String(r[sc]); });\n    var hasCol = rows.some(function (r) { return r.hasOwnProperty(sc); });\n    var ok = true;\n    for (var i = 1; i < vals.length; i++) { var c = vals[i - 1].localeCompare(vals[i], undefined, { sensitivity: 'base' }); if (data.pgSortDesc ? c < 0 : c > 0) { ok = false; break; } }\n    if (!hasCol) { sortNote = ' / ⚠ 列 \"' + sc + '\" が行 JSON に無い (プログラム名を確認)'; }\n    else if (!ok) { sortNote = ' / ⚠ 並び替え未適用 — 列 \"' + sc + '\" に「クリックでソート」(' + (data.pgSortDesc ? '降順' : '昇順') + ') が無いと既定順で返ります'; }\n    else { sortNote = ' / 並び替え適用 (' + (data.pgSortDesc ? '降順' : '昇順') + ')'; }\n  }\n  st.setValue('ページ ' + page + ' (start=' + start + ', count=' + cnt + ') — ' + rows.length + ' 件表示' + (more ? ' / 続きあり' : ' / 末尾') + sortNote + (colNames.length ? ' / 列: ' + colNames.join(', ') : ''));\n})\n.fail(function (xhr, status) { st.setValue('エラー: ' + status + ' — DB パス / ビュー名 / 並び替え列 (クリックソート可能列か) を確認'); });",
                              "customConditional": "show = data.pgHasMore === '1';"
                            }
                          ],
                          "width": 2,
                          "offset": 0,
                          "push": 0,
                          "pull": 0,
                          "size": "md",
                          "currentWidth": 2
                        }
                      ]
                    },
                    {
                      "type": "hidden",
                      "key": "pgStart",
                      "input": true,
                      "persistent": false
                    },
                    {
                      "type": "hidden",
                      "key": "pgHasMore",
                      "input": true,
                      "persistent": false
                    },
                    {
                      "type": "hidden",
                      "key": "pgHasPrev",
                      "input": true,
                      "persistent": false
                    },
                    {
                      "type": "hidden",
                      "key": "pgUrl",
                      "input": true,
                      "persistent": false
                    },
                    {
                      "type": "htmlelement",
                      "tag": "div",
                      "key": "pgUrlDisp",
                      "input": false,
                      "tableView": false,
                      "refreshOnChange": true,
                      "content": "<div class='ks-url'><span class='ks-url-tag'>GET</span>{{ data.pgUrl || '(読み込むと発行 URL がここに出ます — start が進んでいくのを確認できます)' }}</div>"
                    },
                    {
                      "type": "textfield",
                      "key": "pgStatus",
                      "label": "状態",
                      "input": true,
                      "tableView": false,
                      "persistent": false,
                      "disabled": true
                    },
                    {
                      "type": "datagrid",
                      "key": "pgGrid",
                      "label": "現在のページ (persistent:false・保存されません ※設定「persistent:false / Client-only の項目を保存しない」が はい のとき)",
                      "input": true,
                      "tableView": false,
                      "persistent": false,
                      "reorder": false,
                      "addAnotherPosition": "bottom",
                      "disableAddingRemovingRows": true,
                      "clearOnHide": false,
                      "customConditional": "show = !!(data.pgGrid && data.pgGrid.length && data.pgGrid[0] && data.pgGrid[0].unid);",
                      "components": [
                        {
                          "type": "htmlelement",
                          "tag": "div",
                          "key": "pgRowHtml",
                          "label": "列1　列2　列3　(下段: UNID)",
                          "input": false,
                          "tableView": false,
                          "refreshOnChange": true,
                          "content": "<div><strong>{{ row.c1 }}</strong>　{{ row.c2 }}　{{ row.c3 }}<br><span style='font-size:11px;color:#888;'>{{ row.unid }}</span></div>"
                        }
                      ]
                    },
                    {
                      "type": "htmlelement",
                      "tag": "div",
                      "className": "ks-howto",
                      "key": "ptPart2HowTo",
                      "input": false,
                      "tableView": false,
                      "content": "🔧 <strong>流用するには (3 箇所)</strong>: ① ボタンの <code>custom</code> 冒頭で <code>vw</code> をお客様のビュー名に固定 ② 行テンプレート <code>pgRowHtml</code> の <code>{{ row.c1 }}</code>… を実際の列プログラム名 (<code>r['supplierName']</code> など) に置き換え ③ 「行を選ぶ」動作が要るなら ③ の文書ピッカーの「選択」ボタン (<code>row</code> 変数で自行を取得) をこのグリッドに移植。<code>disableAddingRemovingRows:true</code> と <code>persistent:false</code> で「表示専用・保存しない」グリッドになります。"
                    }
                  ]
                },
                {
                  "type": "htmlelement",
                  "tag": "div",
                  "className": "ks-note",
                  "key": "ptSpecIntro",
                  "input": false,
                  "tableView": false,
                  "content": "📎 <strong>URL 組み立ての要点 (実機確認済み)</strong> — 部品を書き換えるときはこの表だけ守れば迷いません。"
                },
                {
                  "type": "htmlelement",
                  "tag": "div",
                  "key": "ptSpecTable",
                  "input": false,
                  "tableView": false,
                  "content": "<table class='ks-spec'>\n<thead><tr><th style='width:24%'>やりたいこと</th><th>書き方</th><th style='width:34%'>注意</th></tr></thead>\n<tbody>\n<tr><td>前方一致で絞る</td><td><code>&amp;keys=&lt;値&gt;</code></td><td>第 1 ソート列が対象。途中一致は不可</td></tr>\n<tr><td>完全一致で 1 件引く</td><td><code>&amp;keys=&lt;値&gt;&amp;keysexactmatch=true</code></td><td><strong>すべて小文字・値は true</strong>。<code>keysExactMatch=1</code> は無視され前方一致になる。多列は <code>keysEx=v1,v2</code></td></tr>\n<tr><td>名称の途中一致 (日本語)</td><td><code>&amp;search=&lt;語&gt;</code></td><td>そのままで部分一致。<code>語*</code> と書くと 0 件になる。全文索引が必要</td></tr>\n<tr><td>名称の途中一致 (英数字)</td><td><code>&amp;search=&lt;語&gt;*</code></td><td>英数字は単語単位一致なので <code>*</code> 必須 (<code>Kir</code> は Kiritani に当たらない)</td></tr>\n<tr><td>特定の項目だけ検索</td><td><code>&amp;search=[項目名] CONTAINS 語</code></td><td>項目名は<strong>文書アイテム名</strong> (ビュー列プログラム名ではない — restDocJson で確認)。ビューに無い項目でも可。<code>search</code> 指定時は <code>keys</code>・<code>category</code> は無視</td></tr>\n<tr><td>複合条件 (AND / OR / NOT)</td><td><code>[A] CONTAINS (x OR y) AND [B] CONTAINS z</code></td><td>Notes 全文検索式そのまま。同一項目の複数語は <code>CONTAINS (x OR y)</code> と括弧で包む (括弧無しの <code>OR y</code> は文書全体に掛かる)。<code>AND NOT</code> で除外、<code>\"語 句\"</code> で連語</td></tr>\n<tr><td>件数・ページング</td><td><code>&amp;count=N&amp;start=M</code></td><td><code>count</code> 省略は 10 件。<code>page&amp;ps</code> も同じ</td></tr>\n<tr><td>並び替え</td><td><code>&amp;sortcolumn=&lt;列名&gt;&amp;sortorder=descending</code></td><td>「クリックでソート」が設定された列 (その方向) のみ。<strong>設定が無い列は黙って既定順で返る</strong> (全文検索と併用時のみエラー)</td></tr>\n<tr><td>ビューに無い項目</td><td>非表示列として追加 / <code>restDocJson.xsp/json?unid=</code></td><td>返るのはビュー全列 (非表示列含む) + <code>@unid</code> 等のメタ列</td></tr>\n</tbody></table>"
                }
              ]
            },
            {
              "type": "panel",
              "key": "ksBlockDocPicker",
              "title": "③ ダイアログで View 検索 → 候補 DataGrid → 選択で親へ反映 (検索モード: 前方一致 / 完全一致 / 全文部分一致 / 項目指定)",
              "customClass": "ks-block ks-docpicker-cell",
              "collapsible": true,
              "collapsed": false,
              "input": false,
              "tableView": false,
              "components": [
                {
                  "type": "htmlelement",
                  "tag": "div",
                  "className": "ks-note",
                  "key": "docPickerNote",
                  "content": "<strong>使いどころ</strong>: 名称の一部で探す取引先・商品・社員の選択。<code>modalEdit:true</code> パネルを開く → ダイアログ内で <code>$.ajax</code> View 検索 → <code>setValue(rows)</code> で DataGrid 表示 → 行の「選択」ボタンが <strong><code>row</code> 変数</strong> (datagrid 行内でのみ定義) で自行データを取り、親フィールドへ <code>setValue</code> して <code>[ref=\"modalSave\"]</code> click でクローズ。再利用部品 <code>dominoViewSearchDialog</code> と同構造です。<br>ダイアログの<strong>検索モード</strong>で URL の組み立てが切り替わります: <strong>前方一致</strong> <code>&amp;keys=</code> / <strong>完全一致</strong> <code>&amp;keys=…&amp;keysexactmatch=true</code> / <strong>全文 部分一致</strong> <code>&amp;search=</code> (日本語はそのまま・英数字は自動で <code>*</code> を付加) / <strong>項目を指定</strong> <code>&amp;search=[項目名] CONTAINS 語</code>。<strong>複合条件</strong>は Notes 全文検索式そのまま: 同じ項目で複数語 <code>[ApplicantKanji] CONTAINS (レヴィ OR 山田)</code> / 別項目との組み合わせ <code>[ApplicantKanji] CONTAINS レヴィ AND [StatusNo] CONTAINS 42</code> / 除外 <code>… AND NOT [StatusNo] CONTAINS 99</code> / 語句 <code>[Title] CONTAINS \"定例 会議\"</code> (数値・日付項目は <code>=</code> <code>&gt;</code> <code>&lt;</code> も可)。<strong>OR と AND を混ぜるときは括弧で明示</strong>。<br>⚠️ <strong><code>[項目名]</code> は文書のアイテム名</strong>です。行 JSON に出る <code>ApplicationNoEx</code> のような<strong>ビュー列プログラム名</strong> (列式で作った値) は文書に存在しないため <code>[ApplicationNoEx] CONTAINS …</code> は常に不成立 (AND で 0 件・OR では前半だけで成立して気付きにくい)。正しいアイテム名は文書エクスプローラ (restDocJson) の全項目 JSON で確認してください。発行 URL はダイアログ内に表示されます。<br>🔧 <strong>流用するには (3 箇所)</strong>: ① <code>btnGridSearch</code> の <code>custom</code> 冒頭で <code>vw</code> (とモード・項目名) をお客様の業務に固定 ② 行テンプレート <code>pickRowHtml</code> の列を実際のプログラム名に ③ 「選択」ボタン <code>btnPickRow</code> の転記先 <code>pickedDocUnid</code> / <code>pickedDocSummary</code> を業務フィールドに。ダイアログ内の入力はすべて <code>persistent:false</code> で、検索結果は Notes に保存されません ※設定「persistent:false / Client-only の項目を保存しない」が はい のとき。",
                  "input": false,
                  "tableView": false
                },
                {
                  "type": "columns",
                  "key": "pickedCols",
                  "label": "Columns",
                  "input": false,
                  "tableView": false,
                  "columns": [
                    {
                      "components": [
                        {
                          "type": "textfield",
                          "key": "pickedDocUnid",
                          "label": "選択された文書 UNID",
                          "input": true,
                          "tableView": false,
                          "disabled": true
                        }
                      ],
                      "width": 4,
                      "offset": 0,
                      "push": 0,
                      "pull": 0,
                      "size": "md",
                      "currentWidth": 4
                    },
                    {
                      "components": [
                        {
                          "type": "textfield",
                          "key": "pickedDocSummary",
                          "label": "選択された文書の概要 (列1〜3)",
                          "input": true,
                          "tableView": false,
                          "disabled": true
                        }
                      ],
                      "width": 5,
                      "offset": 0,
                      "push": 0,
                      "pull": 0,
                      "size": "md",
                      "currentWidth": 5
                    },
                    {
                      "components": [
                        {
                          "type": "panel",
                          "key": "docPickerPanel",
                          "title": "🔍 View を検索して文書を選択",
                          "label": "🔍 View を検索して文書を選択",
                          "modalEdit": true,
                          "input": false,
                          "collapsible": false,
                          "tableView": false,
                          "customClass": "lc-picker ks-docpicker",
                          "components": [
                            {
                              "type": "radio",
                              "key": "gridSearchMode",
                              "label": "検索モード",
                              "input": true,
                              "tableView": false,
                              "persistent": false,
                              "inline": true,
                              "optionsLabelPosition": "right",
                              "defaultValue": "keys",
                              "values": [
                                {
                                  "label": "前方一致 (keys=)",
                                  "value": "keys",
                                  "shortcut": ""
                                },
                                {
                                  "label": "完全一致 (keys=…&keysexactmatch=true)",
                                  "value": "exact",
                                  "shortcut": ""
                                },
                                {
                                  "label": "全文 部分一致 (search=)",
                                  "value": "ft",
                                  "shortcut": ""
                                },
                                {
                                  "label": "項目を指定 (search=[項目] CONTAINS)",
                                  "value": "field",
                                  "shortcut": ""
                                }
                              ]
                            },
                            {
                              "type": "columns",
                              "key": "gridSearchCols",
                              "label": "Columns",
                              "input": false,
                              "tableView": false,
                              "columns": [
                                {
                                  "components": [
                                    {
                                      "type": "textfield",
                                      "key": "gridSearchKey",
                                      "label": "検索語 (空で全件)",
                                      "input": true,
                                      "tableView": false,
                                      "persistent": false,
                                      "placeholder": "前方一致: コードの先頭 / 全文: 名称の一部 / 式: レヴィ OR 山田",
                                      "description": "全文モードでは Notes 全文検索式が使えます: AND / OR / NOT・括弧・\"語 句\"。項目指定モードで A OR B と書くと [項目] CONTAINS (A OR B) に包まれます。[項目] を含む式はそのまま送信"
                                    }
                                  ],
                                  "width": 5,
                                  "offset": 0,
                                  "push": 0,
                                  "pull": 0,
                                  "size": "md",
                                  "currentWidth": 5
                                },
                                {
                                  "components": [
                                    {
                                      "type": "textfield",
                                      "key": "gridSearchField",
                                      "label": "検索する項目名 (Notes アイテム名)",
                                      "input": true,
                                      "tableView": false,
                                      "persistent": false,
                                      "placeholder": "例: supplierName",
                                      "customConditional": "show = data.gridSearchMode === 'field';",
                                      "clearOnHide": false
                                    }
                                  ],
                                  "width": 4,
                                  "offset": 0,
                                  "push": 0,
                                  "pull": 0,
                                  "size": "md",
                                  "currentWidth": 4
                                },
                                {
                                  "components": [
                                    {
                                      "type": "number",
                                      "key": "gridSearchCount",
                                      "label": "件数 (最大 100)",
                                      "input": true,
                                      "tableView": false,
                                      "persistent": false,
                                      "defaultValue": 20,
                                      "validate": {
                                        "min": 1,
                                        "max": 100
                                      }
                                    }
                                  ],
                                  "width": 3,
                                  "offset": 0,
                                  "push": 0,
                                  "pull": 0,
                                  "size": "md",
                                  "currentWidth": 3
                                }
                              ]
                            },
                            {
                              "type": "columns",
                              "key": "gridSearchCols2",
                              "label": "Columns",
                              "input": false,
                              "tableView": false,
                              "columns": [
                                {
                                  "components": [
                                    {
                                      "type": "textfield",
                                      "key": "gridSortColumn",
                                      "label": "並び替え列 (任意・プログラム名)",
                                      "input": true,
                                      "tableView": false,
                                      "persistent": false,
                                      "placeholder": "例: ApplicationNoEx"
                                    }
                                  ],
                                  "width": 4,
                                  "offset": 0,
                                  "push": 0,
                                  "pull": 0,
                                  "size": "md",
                                  "currentWidth": 4
                                },
                                {
                                  "components": [
                                    {
                                      "type": "checkbox",
                                      "key": "gridSortDesc",
                                      "label": "降順",
                                      "input": true,
                                      "tableView": false,
                                      "persistent": false,
                                      "customClass": "ks-run-btn"
                                    }
                                  ],
                                  "width": 2,
                                  "offset": 0,
                                  "push": 0,
                                  "pull": 0,
                                  "size": "md",
                                  "currentWidth": 2
                                },
                                {
                                  "components": [
                                    {
                                      "type": "button",
                                      "key": "btnGridSearch",
                                      "label": "🔍 ビュー検索",
                                      "action": "custom",
                                      "theme": "info",
                                      "customClass": "ks-run-btn",
                                      "input": true,
                                      "tableView": false,
                                      "showValidations": false,
                                      "custom": "XSP.allowSubmit();  // async 操作前に必須\nif (typeof cfGlobal === 'undefined') { alert('consentFlow 実行環境でのみ動作します'); return; }\nvar db = (data.expDbName || '').trim() || 'masterdb';\nvar vw = (data.expViewName || '').trim();\nvar base = './restViewJson.xsp/json?srv=masterdb&db=' + encodeURIComponent(db) + '&vw=' + encodeURIComponent(vw);\nif (!vw) { alert('View エクスプローラの「DB パス」「ビュー名」を先に入力してください'); return; }\nvar cnt = parseInt(data.gridSearchCount, 10); if (!(cnt > 0)) { cnt = 20; } if (cnt > 100) { cnt = 100; }\nvar mode = data.gridSearchMode || 'keys';\nvar kw = (data.gridSearchKey || '').trim();\nvar fld = (data.gridSearchField || '').trim();\nvar url = base + '&count=' + cnt;\n// ---- 検索モードごとの URL 組み立て (実機確認済みの書き方)\nif (kw) {\n  if (mode === 'keys') {\n    url += '&keys=' + encodeURIComponent(kw);                                   // 第 1 ソート列の前方一致\n  } else if (mode === 'exact') {\n    url += '&keys=' + encodeURIComponent(kw) + '&keysexactmatch=true';          // 完全一致 — 小文字 + true 固定 (keysExactMatch=1 は効かない)\n  } else {\n    var term = kw;\n    // 検索語に FT 演算子 (AND / OR / NOT / ACCRUE / NEAR)・括弧・引用符・[項目] が含まれていれば「式」として扱う\n    var isExpr = /(^|\\s)(AND|OR|NOT|ACCRUE|NEAR)(\\s|$)|[()\\[\\]\"]/i.test(term);\n    var hasField = /\\[[^\\]]+\\]|(^|\\s)FIELD\\s+\\S+/i.test(term);                      // [項目名] または FIELD 項目名 を含む = 完全な式 (項目指定モードでも包まない)\n    if (!isExpr && /^[\\x20-\\x7e]+$/.test(term) && !/[*?]$/.test(term)) { term += '*'; }   // 英数字の単語は単語単位一致なので * を付ける。日本語はそのまま (付けると 0 件)\n    if (mode === 'field' && !hasField) {\n      if (!fld) { alert('「項目を指定」モードでは検索する項目名を入力してください'); return; }\n      if (isExpr) { term = '(' + term + ')'; }                                       // [項目] CONTAINS (A OR B) — 括弧無しだと OR が文書全体に掛かる\n      url += '&search=' + encodeURIComponent('[' + fld + '] CONTAINS ' + term); // 項目指定の全文検索 (ビューに無い項目でも可)\n    } else {\n      url += '&search=' + encodeURIComponent(term);                             // 全文検索式そのまま (search 指定時は keys/category は無視される)\n    }\n  }\n} else if (mode === 'exact' || mode === 'field') {\n  alert('このモードでは検索語が必要です'); return;\n}\nvar sc = (data.gridSortColumn || '').trim();\nif (sc) { url += '&sortcolumn=' + encodeURIComponent(sc) + '&sortorder=' + (data.gridSortDesc ? 'descending' : 'ascending'); }\n// clear-then-set: 応答前に前回結果をクリア (古い値の残留防止)\nvar grid = form.getComponent('resultGrid');\nif (grid) { grid.setValue([]); }\nform.getComponent('gridSearchStatus').setValue('検索中...');\nvar _ud = form.getComponent('gridSearchUrl');\nif (_ud) { _ud.setValue(url); }\n$.ajax({ async: true, url: url, method: 'GET', timeout: 30000 })\n.done(function (res) {\n  var rows = (res || []).filter(function (r) { return r && r['@category'] !== true; });\n  if (rows.length === 0) { form.getComponent('gridSearchStatus').setValue('(該当なし)' + (mode === 'ft' || mode === 'field' ? ' — [項目名] は文書アイテム名 (ビュー列プログラム名ではない・文書エクスプローラで確認) / 英数字は 14* のように * を付ける / 全文索引の有無' : '')); return; }\n  var colNames = Object.keys(rows[0]).filter(function (k) { return k.charAt(0) !== '@'; }).slice(0, 3);\n  var toText = function (v) { return (v === undefined || v === null) ? '' : String(v); };\n  var gridRows = rows.map(function (r) {\n    return { unid: toText(r['@unid']), c1: colNames[0] ? toText(r[colNames[0]]) : '', c2: colNames[1] ? toText(r[colNames[1]]) : '', c3: colNames[2] ? toText(r[colNames[2]]) : '' };\n  });\n  var g = form.getComponent('resultGrid');\n  if (g) { g.setValue(gridRows); }\n  var mapTxt = colNames.map(function (c, i) { return '列' + (i + 1) + '=' + c; }).join(', ');\n  // 並び替え適用の簡易判定 (クリックソート不可の列はサーバーが黙って無視するため)\n  var sortNote = '';\n  if (sc && rows.length > 1 && !(mode === 'ft' || mode === 'field')) {\n    var vals = rows.map(function (r) { return (r[sc] === undefined || r[sc] === null) ? '' : String(r[sc]); });\n    var ok = true;\n    for (var i = 1; i < vals.length; i++) { var cmp = vals[i - 1].localeCompare(vals[i], undefined, { sensitivity: 'base' }); if (data.gridSortDesc ? cmp < 0 : cmp > 0) { ok = false; break; } }\n    sortNote = ok ? ' / 並び替え適用' : ' / ⚠ 並び替え未適用 (列 \"' + sc + '\" に「クリックでソート」が無い可能性)';\n  }\n  form.getComponent('gridSearchStatus').setValue(gridRows.length + ' 件 / ' + (mapTxt || '(列なし)') + sortNote);\n})\n.fail(function (xhr, status) {\n  var body = String((xhr && xhr.responseText) || '').replace(/<[^>]+>/g, ' ').replace(/\\s+/g, ' ').slice(0, 160);\n  form.getComponent('gridSearchStatus').setValue('エラー: ' + status + (/not full text indexed/i.test(body) ? ' — この DB には全文索引がありません (管理者に作成を依頼)' : ' — DB パス / ビュー名 / 並び替え列を確認') );\n});"
                                    }
                                  ],
                                  "width": 3,
                                  "offset": 0,
                                  "push": 0,
                                  "pull": 0,
                                  "size": "md",
                                  "currentWidth": 3
                                },
                                {
                                  "components": [
                                    {
                                      "type": "button",
                                      "key": "btnGridClear",
                                      "label": "クリア",
                                      "action": "custom",
                                      "theme": "secondary",
                                      "customClass": "ks-run-btn",
                                      "input": true,
                                      "tableView": false,
                                      "showValidations": false,
                                      "custom": "form.getComponent('gridSearchKey').setValue('');\nform.getComponent('gridSearchStatus').setValue('');\nvar grid = form.getComponent('resultGrid');\nif (grid) { grid.setValue([]); }\nvar f = form.getComponent('gridSearchField'); if (f) { f.setValue(''); }"
                                    }
                                  ],
                                  "width": 3,
                                  "offset": 0,
                                  "push": 0,
                                  "pull": 0,
                                  "size": "md",
                                  "currentWidth": 3
                                }
                              ]
                            },
                            {
                              "type": "textfield",
                              "key": "gridSearchStatus",
                              "label": "件数 / 列マッピング (列1〜3 = View 先頭 3 列)",
                              "input": true,
                              "tableView": false,
                              "disabled": true,
                              "persistent": false
                            },
                            {
                              "type": "htmlelement",
                              "tag": "div",
                              "key": "gridSearchUrlDisp",
                              "input": false,
                              "tableView": false,
                              "refreshOnChange": true,
                              "content": "<div class='ks-url'><span class='ks-url-tag'>GET</span>{{ data.gridSearchUrl || '(検索を実行すると発行 URL がここに表示されます)' }}</div>"
                            },
                            {
                              "type": "hidden",
                              "key": "gridSearchUrl",
                              "input": true,
                              "persistent": false
                            },
                            {
                              "type": "datagrid",
                              "key": "resultGrid",
                              "label": "検索結果（選択候補）— 利用する行の［選択］を押してください",
                              "input": true,
                              "tableView": false,
                              "reorder": false,
                              "persistent": false,
                              "defaultValue": [],
                              "disableAddingRemovingRows": true,
                              "initEmpty": false,
                              "components": [
                                {
                                  "type": "button",
                                  "key": "btnPickRow",
                                  "label": "選択",
                                  "action": "custom",
                                  "theme": "info",
                                  "input": true,
                                  "tableView": false,
                                  "showValidations": false,
                                  "custom": "// `row` は datagrid 行内でのみ定義される自行データ\nvar unid = (row && row.unid) || '';\nif (!unid) { alert('行データがありません'); return; }\n// ★ modalSave への参照は最初に確保する — 後段でグリッドをクリアすると行が再描画され、\n//    instance.element が DOM から外れて closest() が null になり、ダイアログが閉じなくなる\nvar modalRoot = instance.element ? instance.element.closest('.formio-dialog-content') : null;\nvar saveBtn = modalRoot ? modalRoot.querySelector('[ref=\"modalSave\"]') : null;\n// 親フォームのフィールドへ反映\nvar target = form.getComponent('pickedDocUnid');\nif (!target) { alert('組込先フォームに pickedDocUnid / pickedDocSummary フィールドを用意してください'); return; }\ntarget.setValue(unid);\nvar parts = [row.c1, row.c2, row.c3].filter(function (v) { return v !== undefined && v !== null && v !== ''; });\nvar sum = form.getComponent('pickedDocSummary');\nif (sum) { sum.setValue(parts.join(' / ')); }\n// おまけ: 文書エクスプローラの UNID 欄にも自動セット (そのまま全項目を読める)\nvar eu = form.getComponent('expUnid');\nif (eu) { try { eu.setValue(unid); } catch (e) {} }\n// 先に閉じる (modalClose は「変更内容は保存されません」確認が出るので必ず modalSave)\nif (saveBtn) { saveBtn.click(); }\n// 後始末はクローズ後 — グリッドは persistent:false なので保存には乗らない\nvar grid = form.getComponent('resultGrid');\nif (grid) { try { grid.setValue([]); } catch (e) {} }\nvar st = form.getComponent('gridSearchStatus');\nif (st) { try { st.setValue(''); } catch (e) {} }"
                                },
                                {
                                  "type": "htmlelement",
                                  "tag": "div",
                                  "key": "pickRowHtml",
                                  "label": "列1　列2　列3　(下段: UNID)",
                                  "refreshOnChange": false,
                                  "input": false,
                                  "tableView": false,
                                  "attrs": [
                                    {
                                      "attr": "",
                                      "value": ""
                                    }
                                  ],
                                  "content": "<strong>{{ row.c1 || '(値なし)' }}</strong>　{{ row.c2 }}　{{ row.c3 }}<br/><span style='font-size:10px;color:#888;'>{{ row.unid }}</span>"
                                }
                              ],
                              "customConditional": "show = !!(data.resultGrid && data.resultGrid.length && data.resultGrid[0] && data.resultGrid[0].unid);",
                              "description": "検索するとここに結果が表示されます (実データがある時のみ表示する customConditional + clearOnHide:false — false にしないと非表示中の setValue が拒否され表示されない)",
                              "clearOnHide": false
                            },
                            {
                              "type": "button",
                              "key": "btnPickerClose",
                              "label": "閉じる",
                              "action": "custom",
                              "theme": "secondary",
                              "input": true,
                              "tableView": false,
                              "showValidations": false,
                              "custom": "// ★ 参照確保 → click → 後始末 の順 (クリアによる再描画で closest が null になるのを防ぐ)\nvar modalRoot = instance.element ? instance.element.closest('.formio-dialog-content') : null;\nvar saveBtn = modalRoot ? modalRoot.querySelector('[ref=\"modalSave\"]') : null;\nif (saveBtn) { saveBtn.click(); }\nform.getComponent('gridSearchKey').setValue('');\nform.getComponent('gridSearchStatus').setValue('');\nvar grid = form.getComponent('resultGrid');\nif (grid) { grid.setValue([]); }"
                            }
                          ]
                        }
                      ],
                      "width": 3,
                      "offset": 0,
                      "push": 0,
                      "pull": 0,
                      "size": "md",
                      "currentWidth": 3
                    }
                  ]
                }
              ]
            },
            {
              "type": "panel",
              "key": "ksBlockDocExp",
              "title": "📄 restDocJson — 文書エクスプローラ",
              "customClass": "ks-block",
              "collapsible": true,
              "collapsed": false,
              "input": false,
              "tableView": false,
              "components": [
                {
                  "type": "htmlelement",
                  "tag": "div",
                  "className": "ks-note",
                  "key": "docNote",
                  "content": "上の行 JSON の <code>@unid</code> をコピーして貼ると、<code>restDocJson.xsp/json?unid=</code> で文書の<strong>全アイテム</strong> (View に出していない値も) を取得します。RichText は HTML 変換された文字列、複数値は配列で返ります。DB パスは View エクスプローラの入力を共用します。",
                  "input": false,
                  "tableView": false
                },
                {
                  "type": "columns",
                  "key": "docCols",
                  "label": "Columns",
                  "input": false,
                  "tableView": false,
                  "columns": [
                    {
                      "components": [
                        {
                          "type": "textfield",
                          "key": "expUnid",
                          "label": "UNID (32 桁)",
                          "input": true,
                          "tableView": false,
                          "persistent": false,
                          "placeholder": "行 JSON の @unid を貼り付け"
                        }
                      ],
                      "width": 4,
                      "offset": 0,
                      "push": 0,
                      "pull": 0,
                      "size": "md",
                      "currentWidth": 4
                    },
                    {
                      "components": [
                        {
                          "type": "button",
                          "key": "btnDocRead",
                          "label": "📄 文書を読む",
                          "action": "custom",
                          "theme": "primary",
                          "input": true,
                          "tableView": false,
                          "showValidations": false,
                          "custom": "XSP.allowSubmit();\nif (typeof cfGlobal === 'undefined') { alert('consentFlow 実行環境でのみ動作します'); return; }\nvar db = (data.expDbName || '').trim() || 'masterdb';\nvar unid = (data.expUnid || '').trim();\nif (!/^[0-9A-Fa-f]{32}$/.test(unid)) { alert('32 桁の UNID を入力してください (行 JSON の @unid をコピーできます)'); return; }\nvar url = './restDocJson.xsp/json?srv=masterdb&db=' + encodeURIComponent(db) + '&unid=' + encodeURIComponent(unid);\n// 発行 URL を UI に表示\nvar _ud = form.getComponent('docReadUrl');\nif (_ud) { _ud.setValue(url); }\nform.getComponent('expDocJson').setValue('取得中...');\n$.ajax({ async: true, url: url, method: 'GET', timeout: 30000 })\n.done(function (doc) {\n  if (!doc || !doc['@unid']) { form.getComponent('expDocJson').setValue('(該当なし)'); return; }\n  var txt = JSON.stringify(doc, null, 1);\n  form.getComponent('expDocJson').setValue(txt.length > 3000 ? txt.slice(0, 3000) + '\\n...(省略)' : txt);\n}).fail(function (xhr, status) {\n  form.getComponent('expDocJson').setValue('エラー: ' + status);\n});"
                        }
                      ],
                      "width": 3,
                      "offset": 0,
                      "push": 0,
                      "pull": 0,
                      "size": "md",
                      "currentWidth": 3
                    },
                    {
                      "components": [
                        {
                          "type": "textarea",
                          "key": "expDocJson",
                          "label": "文書の全アイテム JSON (保存されません ※設定「persistent:false / Client-only の項目を保存しない」が はい のとき)",
                          "rows": 6,
                          "input": true,
                          "tableView": false,
                          "disabled": true,
                          "persistent": false
                        }
                      ],
                      "width": 5,
                      "offset": 0,
                      "push": 0,
                      "pull": 0,
                      "size": "md",
                      "currentWidth": 5
                    }
                  ]
                },
                {
                  "type": "htmlelement",
                  "tag": "div",
                  "key": "docUrlDisp",
                  "input": false,
                  "tableView": false,
                  "refreshOnChange": true,
                  "content": "<div class='ks-url'><span class='ks-url-tag'>GET</span>{{ data.docReadUrl || '(文書を読むと発行 URL がここに表示されます)' }}</div>"
                },
                {
                  "type": "hidden",
                  "key": "docReadUrl",
                  "input": true,
                  "persistent": false
                }
              ]
            },
            {
              "type": "htmlelement",
              "tag": "div",
              "className": "ks-note",
              "key": "prefixNote",
              "content": "💡 その他の鉄則: 単一文書の全項目は <code>restDocJson.xsp/json?unid=</code>。32,000 バイト超の文字列値は黙って RichText 化されビュー列から消えるため、serialized JSON hidden には 30,000 バイト上限を張ります。",
              "input": false,
              "tableView": false
            }
          ]
        }
      ]
    },
    {
      "type": "panel",
      "key": "chartPanel",
      "title": "📈 Chart.js v2 連動チャート",
      "customClass": "ks-chartpanel",
      "collapsible": true,
      "collapsed": false,
      "input": false,
      "tableView": false,
      "components": [
        {
          "type": "htmlelement",
          "tag": "div",
          "className": "ks-note",
          "key": "chartNote",
          "content": "Chart.js v2 + plugin はコアが全ページに自動ロード済み — CDN 読込不要で <code>Chart</code> グローバルが使えます (v3/v4 API は不可)。下のグリッドを編集するとチャートが 300ms debounce で再描画されます (logic[] trigger:event \"change\" + customAction)。",
          "input": false,
          "tableView": false
        },
        {
          "type": "datagrid",
          "key": "chartData",
          "label": "チャート データ",
          "input": true,
          "tableView": false,
          "reorder": false,
          "addAnother": "＋ 行を追加",
          "defaultValue": [
            {
              "chartLabel": "第1四半期",
              "chartVal": 120
            },
            {
              "chartLabel": "第2四半期",
              "chartVal": 150
            },
            {
              "chartLabel": "第3四半期",
              "chartVal": 90
            },
            {
              "chartLabel": "第4四半期",
              "chartVal": 180
            }
          ],
          "components": [
            {
              "type": "textfield",
              "key": "chartLabel",
              "label": "項目名",
              "input": true,
              "tableView": true
            },
            {
              "type": "number",
              "key": "chartVal",
              "label": "値",
              "input": true,
              "tableView": true
            }
          ]
        },
        {
          "type": "htmlelement",
          "key": "chartContainer",
          "tag": "div",
          "input": false,
          "tableView": false,
          "content": "<div style='border:1px solid #ddd;border-radius:8px;overflow:hidden;background:#fff;'><div class='chart-grid'><div class='chart-card'><h4>📊 棒グラフ (bar)</h4><div class='chart-wrapper'><canvas id='ksChartBar'></canvas></div></div><div class='chart-card'><h4>🍩 ドーナツ (doughnut)</h4><div class='chart-wrapper'><canvas id='ksChartDoughnut'></canvas></div></div></div></div>",
          "logic": [
            {
              "name": "renderKsCharts",
              "trigger": {
                "type": "event",
                "event": "change"
              },
              "actions": [
                {
                  "name": "drawCharts",
                  "type": "customAction",
                  "customAction": "if (window._ksChartPending) { return; }\nwindow._ksChartPending = true;\nif (window._ksChartTimer) { clearTimeout(window._ksChartTimer); }\nwindow._ksChartTimer = setTimeout(function () {\n  window._ksChartPending = false;\n  try {\n    if (typeof Chart === 'undefined') { return; }\n    var rows = (data || {}).chartData || [];\n    var labels = []; var vals = [];\n    for (var i = 0; i < rows.length; i++) { labels.push(rows[i].chartLabel || ('項目' + (i + 1))); vals.push(rows[i].chartVal || 0); }\n    var palette = ['#1e88e5', '#fb8c00', '#00acc1', '#7cb342', '#9c27b0', '#f44336', '#795548', '#607d8b'];\n    var cs = []; for (var c = 0; c < labels.length; c++) { cs.push(palette[c % palette.length]); }\n    var bar = document.getElementById('ksChartBar');\n    if (bar) {\n      if (window._ksChartBar) { window._ksChartBar.destroy(); window._ksChartBar = null; }\n      window._ksChartBar = new Chart(bar, { type: 'bar', data: { labels: labels, datasets: [{ label: '値', data: vals, backgroundColor: cs }] }, options: { responsive: true, maintainAspectRatio: false, animation: false, legend: { display: false }, scales: { xAxes: [{ gridLines: { display: false } }], yAxes: [{ ticks: { beginAtZero: true }, gridLines: { color: '#eee' } }] } } });\n    }\n    var dn = document.getElementById('ksChartDoughnut');\n    if (dn) {\n      if (window._ksChartDoughnut) { window._ksChartDoughnut.destroy(); window._ksChartDoughnut = null; }\n      window._ksChartDoughnut = new Chart(dn, { type: 'doughnut', data: { labels: labels, datasets: [{ data: vals, backgroundColor: cs, borderWidth: 2, borderColor: '#fff' }] }, options: { responsive: true, maintainAspectRatio: false, animation: false, legend: { position: 'right', labels: { usePointStyle: true, padding: 12 } } } });\n    }\n  } catch (e) { console.error('ksChart error:', e); }\n}, 300);"
                }
              ]
            }
          ]
        }
      ]
    }
  ]
}
