{
  "schema": 1,
  "about": "ui-kit の部品カタログの台帳。画面(catalog/index.html)もAIも、このファイル1つを読む。部品を足す・直すときは、ここを先に直す。",
  "howToRefer": {
    "idFormat": "部品ID-バリエーション文字。例: BTN-A(ボタンの主ボタン)。部品IDだけ(BTN)は、その部品の全バリエーション。",
    "roles": "バリエーションは、役割で分ける(主・副・破壊的など)。色や、Web・React の違いでは分けない。Web と React で同じ役割のものは、1つのバリエーションにまとめ、code(Web)と react(React)を両方持つ。WebアプリとiOSは、見た目の作法が違うので、family(webapp / ios)で区別し、同じ役割でも別のバリエーションにする。",
    "whenUserSays": "ユーザーが「BTN-A をここに反映して」と言ったら: ①この台帳で id を引く ②反映先のアプリに合う実装を選ぶ(React のアプリなら react、そうでなければ code。iOS 風のアプリなら family が ios のもの)③部品の impls の files を読んで、中身を確かめる ④code(Web)または react(React)をそのまま使い、アプリ側に色・形を書かない ⑤見た目の違いは web/tokens.css の値で出す。",
    "ifNoImpl": "反映先に合う実装が無いとき(例: React のアプリで、Web だけのバリエーション)は、勝手に作り直さず、先にユーザーに伝える。"
  },
  "families": [
    {
      "id": "webapp",
      "name": "Webアプリ"
    },
    {
      "id": "ios",
      "name": "iOS"
    },
    {
      "id": "system",
      "name": "仕組み"
    }
  ],
  "categories": [
    {
      "id": "操作",
      "name": "操作"
    },
    {
      "id": "グラフ",
      "name": "グラフ・表示"
    },
    {
      "id": "一覧",
      "name": "一覧・行"
    },
    {
      "id": "重ねる",
      "name": "シート・ダイアログ"
    },
    {
      "id": "入力",
      "name": "入力"
    },
    {
      "id": "動き",
      "name": "動き・スクロール"
    },
    {
      "id": "デザイン",
      "name": "デザインの決まり(Apple)"
    },
    {
      "id": "操作表現",
      "name": "操作の見せ方"
    },
    {
      "id": "仕組み",
      "name": "仕組み(見た目なし)"
    }
  ],
  "parts": [
    {
      "id": "BTN",
      "name": "ボタン",
      "category": "操作",
      "family": "webapp",
      "preview": "web",
      "aliases": [
        "button",
        "ピル",
        "ボタン",
        "保存",
        "押す"
      ],
      "summary": "押す部品。形は全部ピル型。役割で選ぶ(色でなく、主・副・破壊的など)。Web と React は同じ役割の同じ見た目で、実装だけが違う。",
      "tags": [
        "タップ",
        "ピル",
        "基本"
      ],
      "usedBy": [
        "Hanbun",
        "unilife"
      ],
      "impls": [
        {
          "platform": "web",
          "call": "UI.btn(種類, 属性, 中身, {fill, grow, cls, state})、UI.run(ボタン, 処理)",
          "files": [
            "web/ui.js",
            "web/ui.css",
            "web/tokens.css"
          ],
          "usage": "aria-pressed などの true・false は、文字の \"true\"・\"false\" になる。画面に色・形を書かない。"
        },
        {
          "platform": "react",
          "call": "className={btn / btnTint / btnInvert / btnSub / btnDanger}",
          "files": [
            "react/ui.tsx"
          ],
          "usage": "アプリ側に色の決まり(bg-accent など)と共通CSSが要る。README の「前提」を参照。"
        }
      ],
      "variants": [
        {
          "id": "A",
          "name": "主ボタン",
          "key": "目立つ操作(1画面に1〜2つ)",
          "code": "UI.btn('primary',{},'保存')",
          "react": "<button className={btn}>保存</button>",
          "gesture": {
            "type": "tap",
            "target": "button.u"
          },
          "tags": [
            "保存",
            "塗り"
          ]
        },
        {
          "id": "B",
          "name": "副ボタン",
          "key": "キャンセルなど(標準・小)",
          "code": "UI.btn('secondary',{},'キャンセル')+UI.btn('outline',{},'予算を変更')",
          "react": "<button className={btnSub}>キャンセル</button>",
          "gesture": {
            "type": "tap",
            "target": "button.u"
          },
          "tags": [
            "枠線",
            "確認"
          ]
        },
        {
          "id": "C",
          "name": "淡いボタン",
          "key": "補助の操作(React のみ)",
          "code": "<button className={btnTint}>追加</button>",
          "preview": "react-static",
          "tags": [
            "塗り"
          ],
          "look": "淡い色の塗りのピル、アクセント色の文字"
        },
        {
          "id": "D",
          "name": "反転ボタン",
          "key": "暗い背景の上の目立つ操作",
          "code": "UI.btn('light',{},'精算')",
          "react": "<button className={btnInvert}>実行</button>",
          "stage": "dark",
          "gesture": {
            "type": "tap",
            "target": "button.u"
          },
          "tags": [
            "暗い背景",
            "塗り"
          ]
        },
        {
          "id": "E",
          "name": "破壊的ボタン",
          "key": "削除・解除(赤)",
          "code": "UI.btn('danger',{},'削除')",
          "react": "<button className={btnDanger}>削除</button>",
          "gesture": {
            "type": "tap",
            "target": "button.u"
          },
          "tags": [
            "削除",
            "赤",
            "塗り"
          ],
          "note": "削除・解除は、赤いピルに白い文字。主ボタンにはしない。"
        },
        {
          "id": "F",
          "name": "文字ボタン",
          "key": "背景なし(月の移動など)",
          "code": "UI.btn('plain',{},'<b>2026年10月</b>')",
          "gesture": {
            "type": "tap",
            "target": "button.u"
          },
          "tags": [
            "文字のみ"
          ]
        },
        {
          "id": "G",
          "name": "丸アイコンボタン",
          "key": "閉じる・完了",
          "code": "UI.btn('glass',{'aria-label':'閉じる'},icon('xmark'))+UI.btn('confirm',{'aria-label':'完了'},icon('check'))",
          "stage": "scene",
          "gesture": {
            "type": "tap",
            "target": "button.u",
            "index": 1
          },
          "tags": [
            "ガラス",
            "丸",
            "閉じる",
            "完了",
            "青"
          ],
          "note": "ガラスの丸(閉じる)と、青い丸(完了)。完了は文字でなくチェックの記号。シートでは、閉じる(左)と完了(右)を対で置く。"
        },
        {
          "id": "H",
          "name": "保存ボタンの押した動き",
          "key": "押す → 丸い回転 → 丸いチェック",
          "code": "UI.btn('primary',{},'保存',{state:true})",
          "gesture": {
            "type": "tap",
            "target": "button.u"
          },
          "loop": {
            "kind": "gesture",
            "ms": 1400
          },
          "tags": [
            "保存",
            "塗り",
            "タップ"
          ],
          "note": "押した瞬間に縮み(.95)、処理が始まると、ボタンが高さと同じ幅の完全な丸に縮み(.32秒)、中で丸い回転。終わると、丸のまま✓が弾んで出る。1.2秒後に、元の形へ戻る。UI.btn(..., {state:true}) と UI.run(ボタン, 処理)。React 版は、まだ無い。"
        },
        {
          "id": "I",
          "name": "ボタンが中身に合わせて伸び縮み",
          "key": "UI.morph(ボタン, 中身を変える関数)",
          "code": "UI.btn('primary',{},'保存')",
          "loop": {
            "kind": "morph",
            "values": [
              "保存",
              "変更を保存",
              "OK",
              "削除して閉じる"
            ],
            "ms": 1600
          },
          "tags": [
            "保存",
            "塗り"
          ],
          "note": "文字やアイコンを変えると、背景のボタンが、新しい中身の大きさまで、なめらかに伸び縮みする(.32秒)。保存ボタンの丸も、この仕組みの一つ。"
        }
      ]
    },
    {
      "id": "SWT",
      "name": "スイッチ",
      "category": "操作",
      "family": "webapp",
      "preview": "web",
      "aliases": [
        "switch",
        "toggle",
        "トグル"
      ],
      "summary": "オン・オフの切り替え。Webアプリ版と、iOS版(緑のトグル、つまみがすべる)がある。",
      "tags": [
        "切り替え",
        "タップ",
        "設定"
      ],
      "usedBy": [
        "Hanbun",
        "unilife"
      ],
      "impls": [
        {
          "platform": "web",
          "call": "UI.sw(オンか, 属性) / Apple.toggle(オンか, ラベル, 属性)",
          "files": [
            "web/ui.js",
            "web/ui.css",
            "web/tokens.css",
            "web/apple.css",
            "web/apple.js"
          ],
          "usage": "aria-checked でつまみの位置が決まる。押したときの切り替えはアプリ側で書く。iOS版は web/apple.css が要る。"
        },
        {
          "platform": "react",
          "call": "<Switch checked onChange label />",
          "files": [
            "react/ui.tsx"
          ],
          "usage": "タップ領域は44pt以上。"
        }
      ],
      "variants": [
        {
          "id": "A",
          "name": "スイッチ(Webアプリ)",
          "key": "オン・オフ(押すと切り替わる)",
          "code": "UI.sw(true,{'aria-label':'通知'})",
          "react": "<Switch checked={on} onChange={set} label=\"通知\" />",
          "gesture": {
            "type": "tap",
            "target": ".u-switch",
            "index": 0
          },
          "loop": {
            "kind": "gesture",
            "ms": 1400
          },
          "tags": [
            "状態違い"
          ],
          "note": "つまみが滑り、色が変わる(200ms)。"
        },
        {
          "id": "B",
          "name": "トグル(iOS)",
          "key": "緑・つまみが弾む",
          "code": "Apple.toggle(true,'通知')",
          "family": "ios",
          "stage": "apple",
          "gesture": {
            "type": "tap",
            "target": ".toggle"
          },
          "loop": {
            "kind": "gesture",
            "ms": 1400
          },
          "tags": [
            "状態違い"
          ],
          "note": "iOS のシステムの緑。つまみは0.25秒ですべる。幅51×高さ31。"
        }
      ]
    },
    {
      "id": "PIL",
      "name": "ピル(選択)",
      "category": "操作",
      "family": "webapp",
      "preview": "web",
      "aliases": [
        "pill",
        "chip",
        "フィルタ",
        "絞り込み",
        "チップ"
      ],
      "summary": "絞り込みや、選択のためのピル。選んでいる状態は、色だけでなく、太い文字でも示す。",
      "tags": [
        "タップ",
        "ピル",
        "絞り込み",
        "選択"
      ],
      "usedBy": [
        "Hanbun",
        "unilife"
      ],
      "impls": [
        {
          "platform": "web",
          "call": "UI.btn('chip' / 'choice', {'aria-pressed': true|false}, 中身)",
          "files": [
            "web/ui.js",
            "web/ui.css",
            "web/tokens.css"
          ],
          "usage": "選択中は aria-pressed=\"true\"。"
        },
        {
          "platform": "react",
          "call": "<Pill selected onClick>、<PillRow label>",
          "files": [
            "react/ui.tsx"
          ],
          "usage": "選択中は、淡い背景と青い枠・青い文字(文字の太さと件数でも示す)。"
        }
      ],
      "variants": [
        {
          "id": "A",
          "name": "選択ピル",
          "key": "未選択・選択中",
          "code": "UI.btn('chip',{'aria-pressed':false},'交通費')+UI.btn('chip',{'aria-pressed':true},'食費')",
          "react": "<Pill selected onClick={f}>食費</Pill>",
          "gesture": {
            "type": "tap",
            "target": "button.u",
            "index": 0
          },
          "tags": [
            "選択",
            "状態違い"
          ]
        },
        {
          "id": "B",
          "name": "色つき選択",
          "key": "人ごとの色(支払った人など)",
          "code": "UI.btn('choice',{'aria-pressed':true},'ユウ')+UI.btn('choice',{'aria-pressed':false},'アオ')",
          "stageStyle": "--pc:#F28DB0;--pb:#F28DB0;--pt:#3A3128",
          "gesture": {
            "type": "tap",
            "target": "button.u",
            "index": 1
          },
          "tags": [
            "選択",
            "色つき",
            "状態違い"
          ],
          "note": "色は --pc(その人の色)、--pb(選んだときの背景)、--pt(選んだときの文字)で渡す。選んだほうは全体がその人の色、選んでいないほうは枠線だけ。"
        },
        {
          "id": "C",
          "name": "ピルの行",
          "key": "横にスクロールする並び(React)",
          "code": "<PillRow label=\"絞り込み\"><Pill …/><Pill …/></PillRow>",
          "preview": "react-static",
          "tags": [
            "絞り込み"
          ],
          "look": "横にスクロールできる並び(バーなし)"
        }
      ]
    },
    {
      "id": "SEG",
      "name": "セグメント",
      "category": "操作",
      "family": "webapp",
      "preview": "web",
      "aliases": [
        "segmented",
        "タブ",
        "切り替え"
      ],
      "summary": "選択肢を横に並べて1つ選ぶ。ピル型で、つまみをドラッグしても、タップしても、矢印キーでも選べる。カタログの「すべて / Webアプリ」「PC / スマホ」も、これ。iOS風の角丸の四角い形もある。",
      "tags": [
        "選択",
        "切り替え",
        "絞り込み"
      ],
      "usedBy": [
        "unilife"
      ],
      "impls": [
        {
          "platform": "web",
          "call": "Apple.seg(選択肢, 値, 名前, 属性, {ios}) + Apple.segBind(根, 関数) + Apple.place(根)",
          "files": [
            "web/apple.css",
            "web/apple.js"
          ],
          "usage": "選択肢は [[値, 文字, アイコンのHTML], ...]。項目は同じ幅。6px を超えて動かすとドラッグ。矢印キーでも選べる。{ios: true} で、iOS の角丸の四角い形。"
        },
        {
          "platform": "react",
          "call": "<Segmented value options onChange label tone />",
          "files": [
            "react/ui.tsx"
          ],
          "usage": "options は { value, label, count? }。tone: solid / soft。Web 版と同じ仕様(ドラッグ、12pxのゴム、バネ)。"
        }
      ],
      "variants": [
        {
          "id": "A",
          "name": "セグメント(Webアプリ)",
          "key": "Apple.seg / <Segmented>",
          "family": "webapp",
          "preview": "web",
          "stage": "apple",
          "code": "Apple.seg([['a','すべて'],['b','Webアプリ'],['c','iOS']],'a','種類')",
          "react": "<Segmented value={v} options={opts} onChange={set} label=\"種類\" />",
          "gesture": {
            "type": "drag",
            "target": ".seg",
            "fx": 0.15,
            "dx": 150,
            "real": true,
            "alt": true
          },
          "loop": {
            "kind": "gesture",
            "ms": 1000
          },
          "tags": [
            "選択",
            "切り替え",
            "絞り込み",
            "ドラッグ"
          ],
          "note": "ドラッグ・タップ・矢印キーのどれでも選べる。つまみを指やポインターでドラッグできる(6pxを超えたらドラッグ。1:1で追従、端は12pxのゴム、離すとバネで近い項目へ吸着)。"
        },
        {
          "id": "B",
          "name": "セグメント(iOS)",
          "key": "Apple.seg {ios: true}",
          "family": "ios",
          "preview": "web",
          "stage": "apple",
          "code": "Apple.seg([['d','日'],['w','週'],['m','月']],'d','表示',{},{ios:true})",
          "gesture": {
            "type": "drag",
            "target": ".seg",
            "fx": 0.17,
            "dx": 110,
            "real": true,
            "alt": true
          },
          "loop": {
            "kind": "gesture",
            "ms": 1000
          },
          "tags": [
            "選択",
            "切り替え",
            "絞り込み",
            "ドラッグ"
          ],
          "note": "iOS の、角丸の四角いセグメント(余白2px、角丸9 / つまみ7)。動きは同じで、ドラッグもできる。"
        }
      ]
    },
    {
      "id": "BDG",
      "name": "バッジ",
      "category": "グラフ",
      "family": "webapp",
      "preview": "react-static",
      "aliases": [
        "badge",
        "ステータス",
        "タグ"
      ],
      "summary": "状態や属性を示す小さなラベル。意味で色を選ぶ(進行中・完了・警告・予定・標準)。",
      "tags": [
        "状態表示",
        "ピル",
        "色つき"
      ],
      "usedBy": [
        "unilife"
      ],
      "impls": [
        {
          "platform": "react",
          "call": "<Badge tone icon onClick>文字</Badge>",
          "files": [
            "react/ui.tsx"
          ],
          "usage": "tone: accent / ok / warn / plan / neutral。CSS は badge-accent など(アプリ側)。"
        }
      ],
      "variants": [
        {
          "id": "A",
          "name": "バッジ(5つの意味)",
          "key": "accent / ok / warn / plan / neutral",
          "code": "<Badge tone=\"accent\">進行中</Badge> <Badge tone=\"ok\">完了</Badge> <Badge tone=\"warn\">期限超過</Badge> <Badge tone=\"plan\">履修予定</Badge> <Badge>対面</Badge>",
          "preview": "react-static",
          "look": "ピル形。地は、その色を薄くしたもの"
        }
      ]
    },
    {
      "id": "CRD",
      "name": "カード",
      "category": "一覧",
      "family": "webapp",
      "preview": "react-static",
      "aliases": [
        "card",
        "面"
      ],
      "summary": "内容をまとめる面。Card は一段高い背景、CardInner はその内側の入れ子(角丸は同心円)。",
      "tags": [
        "一覧",
        "入れ子"
      ],
      "usedBy": [
        "unilife"
      ],
      "impls": [
        {
          "platform": "react",
          "call": "<Card>…</Card>、<CardInner>…</CardInner>",
          "files": [
            "react/ui.tsx"
          ],
          "usage": "角丸は外枠 − 余白(同心円。K128)。"
        }
      ],
      "variants": [
        {
          "id": "A",
          "name": "カードと内側のカード",
          "key": "Card > CardInner",
          "code": "<Card><CardInner>内容</CardInner></Card>",
          "preview": "react-static",
          "look": "一段高い面と、その中の同心円のサブ面"
        }
      ]
    },
    {
      "id": "ROW",
      "name": "リスト行",
      "category": "一覧",
      "family": "webapp",
      "preview": "react-static",
      "aliases": [
        "row",
        "list",
        "設定の行",
        "リスト"
      ],
      "summary": "リストや設定の1行。タップ領域44pt以上。Webアプリ版と、iOS版(インセットのグループ)がある。",
      "tags": [
        "一覧",
        "タップ",
        "設定"
      ],
      "usedBy": [
        "unilife"
      ],
      "impls": [
        {
          "platform": "react",
          "call": "<RowItem>、<SettingNavRow label value onClick />、<SelectList><SelectItem /></SelectList>",
          "files": [
            "react/ui.tsx"
          ],
          "usage": "SelectItem は選択のチェックマーク付き。SettingNavRow は右の矢印付き。"
        },
        {
          "platform": "web",
          "call": "クラス .list-group / .list-row(iOS版)",
          "files": [
            "web/apple.css"
          ],
          "usage": "角丸10、行は44pt以上、区切りは0.5pxで左に16pxの余白。"
        }
      ],
      "variants": [
        {
          "id": "A",
          "name": "リスト行(Webアプリ)",
          "key": "行・設定の行・選択の行",
          "code": "<RowItem>授業名</RowItem> / <SettingNavRow label=\"通知\" value=\"オン\" /> / <SelectItem selected label=\"前期\" />",
          "preview": "react-static",
          "tags": [
            "選択"
          ],
          "look": "押し込み・ホバー付きの行。設定の行は右に値と矢印。選択の行はチェックマーク"
        },
        {
          "id": "B",
          "name": "リスト(iOS)",
          "key": "インセットのグループ",
          "code": "'<div class=\"list-group\" style=\"width:100%\"><div class=\"list-row\"><span class=\"glyph\">'+icon('gear')+'</span><span class=\"ttl\">一般</span></div><div class=\"list-row\"><span class=\"glyph\" style=\"background:#34c759\">'+icon('check')+'</span><span class=\"ttl\">通知</span><span class=\"detail\">オン</span></div></div>'",
          "family": "ios",
          "preview": "web",
          "stage": "apple",
          "tags": [
            "入れ子"
          ]
        }
      ]
    },
    {
      "id": "INP",
      "name": "入力欄",
      "category": "入力",
      "family": "webapp",
      "preview": "react-static",
      "aliases": [
        "input",
        "textarea",
        "search",
        "検索",
        "テキスト"
      ],
      "summary": "検索欄と、入力欄(1行・複数行)。検索欄は、虫眼鏡・入力・入力した文字をクリアする×ボタンまでが1つの部品(文字が入ると×が出て、押すと消えて入力欄に戻る。Esc でも消える)。",
      "tags": [
        "入力"
      ],
      "usedBy": [
        "unilife"
      ],
      "impls": [
        {
          "platform": "web",
          "call": "Apple.search({placeholder, label, value}) + Apple.searchBind(根, (値, 入力欄) => {...})",
          "files": [
            "web/apple.css",
            "web/apple.js"
          ],
          "usage": "HTML を作って、根に一度だけ searchBind。入力もクリアも、同じ関数で受け取る(クリアは、空の入力として伝わる)。ピル形・44pt。"
        },
        {
          "platform": "react",
          "call": "<SearchField value onChange placeholder label />、<Input />、<Textarea />",
          "files": [
            "react/ui.tsx"
          ],
          "usage": "フォーカスは入力欄に残す。"
        }
      ],
      "variants": [
        {
          "id": "A",
          "name": "検索欄(クリアの×つき)",
          "key": "Apple.search / <SearchField>",
          "family": "webapp",
          "preview": "web",
          "stage": "apple",
          "code": "Apple.search({placeholder:'検索'})",
          "react": "<SearchField value={q} onChange={setQ} placeholder=\"授業名\" />",
          "loop": {
            "kind": "type",
            "text": "ボタン",
            "ms": 1200
          },
          "tags": [
            "検索",
            "入力",
            "絞り込み"
          ],
          "note": "文字が入ると×が出て、押すと、消えて入力欄に戻る(消したことも、入力として伝わる)。Esc でも消える。見本は、文字を入れて×を押す。"
        },
        {
          "id": "B",
          "name": "入力欄(1行・複数行)",
          "key": "Input / Textarea",
          "code": "<Input placeholder=\"教室\" /> / <Textarea placeholder=\"メモ\" />",
          "preview": "react-static",
          "look": "角丸の入力欄"
        }
      ]
    },
    {
      "id": "HBR",
      "name": "横棒グラフ",
      "category": "グラフ",
      "family": "webapp",
      "preview": "web",
      "aliases": [
        "bar",
        "progress",
        "進捗",
        "メーター"
      ],
      "summary": "進み具合を1本の横棒で見せる(積み上げ対応)。値・補った分・予定の順。仕様は docs/ の横棒グラフの文書。",
      "tags": [
        "進捗",
        "状態表示"
      ],
      "usedBy": [
        "unilife"
      ],
      "impls": [
        {
          "platform": "web",
          "call": "UI.bar({ value, supplied, planned, max, label, tone })",
          "files": [
            "web/ui.js",
            "web/ui.css",
            "web/tokens.css",
            "docs/横棒グラフ.md"
          ],
          "usage": "色は --bar-main / --bar-supplied / --bar-planned / --bar-warn。必ず数字と凡例を併記する。"
        },
        {
          "platform": "react",
          "call": "<Bar value supplied planned max label tone />",
          "files": [
            "react/ui.tsx",
            "react/bar-logic.ts",
            "docs/横棒グラフ.md"
          ],
          "usage": "長さの計算は bar-logic.ts。"
        }
      ],
      "variants": [
        {
          "id": "A",
          "name": "積み上げ(値・補い・予定)",
          "key": "UI.bar",
          "code": "UI.bar({value:40,supplied:10,planned:15,max:124,label:'取得 + 補った分 + 予定'})",
          "react": "<Bar value={40} supplied={10} planned={15} max={124} label=\"…\" />"
        },
        {
          "id": "B",
          "name": "警告・超過",
          "key": "tone: warn / 合計 > max",
          "code": "'<div style=\"width:100%;display:grid;gap:.9rem\">'+UI.bar({value:60,planned:20,max:124,tone:'warn',label:'警告'})+UI.bar({value:100,planned:40,max:120,label:'超過'})+'</div>'",
          "tags": [
            "赤"
          ],
          "note": "警告は赤。合計が max を超えたら、全部埋まる。"
        },
        {
          "id": "C",
          "name": "伸びる動き",
          "key": "表示したとき(繰り返し)",
          "code": "UI.bar({value:40,supplied:10,planned:15,max:124,label:'取得 + 補った分 + 予定'})",
          "loop": {
            "kind": "replay",
            "ms": 3200
          },
          "note": "左から600msで伸びる。"
        },
        {
          "id": "D",
          "name": "値が変わる動き",
          "key": "値の変更(繰り返し)",
          "code": "UI.bar({value:40,supplied:10,planned:15,max:124,label:'取得 + 補った分 + 予定'})",
          "loop": {
            "kind": "style",
            "selector": ".u-bar-fill",
            "prop": "width",
            "values": [
              "25%",
              "90%"
            ],
            "ms": 1800
          },
          "note": "幅が450msでなめらかに動く。"
        }
      ]
    },
    {
      "id": "VBR",
      "name": "積み上げ棒グラフ",
      "category": "グラフ",
      "family": "webapp",
      "preview": "web",
      "aliases": [
        "chart",
        "bars",
        "棒グラフ",
        "縦棒"
      ],
      "summary": "カテゴリごとに下から積み上がる縦の棒グラフ。縦軸・補助線・基準線・凡例つき。カテゴリを選ぶと、棒・円・凡例のすべてが、選んだカテゴリだけ残る。",
      "tags": [
        "統計",
        "積み上げ",
        "選択"
      ],
      "usedBy": [
        "Hanbun"
      ],
      "impls": [
        {
          "platform": "web",
          "call": "Chart.bars({ cols, colW, hi, step, order, color, selected, act, guide, ... })",
          "files": [
            "web/chart.js",
            "web/chart.css",
            "web/ui.css",
            "web/tokens.css"
          ],
          "usage": "動かすときは祖先に ch-anim を付けて描き直す。選択だけ変えるときは Chart.select(root, カテゴリ名)。引数の意味は web/chart.js の冒頭。DEMO はカタログの見本データ。"
        }
      ],
      "variants": [
        {
          "id": "A",
          "name": "積み上げ棒(基準線・選択)",
          "key": "Chart.bars",
          "code": "Chart.bars({ cols: DEMO.months, colW: 36, inset: 6, hi: DEMO.hi, step: DEMO.step, order: DEMO.order, color: DEMO.color, guide: { value: 30000, label: '予算' }, act: { cat: 'cat', col: 'col' } })",
          "gesture": {
            "type": "tap",
            "target": ".lgd",
            "index": 1
          },
          "note": "動かすときは祖先に ch-anim を付けて描き直す。選択だけ変えるときは Chart.select。DEMO はカタログの見本データ。"
        },
        {
          "id": "B",
          "name": "下から伸びる動き",
          "key": "ch-anim(繰り返し)",
          "code": "Chart.bars({ cols: DEMO.months, colW: 36, inset: 6, hi: DEMO.hi, step: DEMO.step, order: DEMO.order, color: DEMO.color, guide: { value: 30000, label: '予算' }, act: { cat: 'cat', col: 'col' } })",
          "loop": {
            "kind": "replay",
            "ms": 3600
          },
          "note": "棒が下から1本ずつ(35msずつずらして)伸びる。"
        },
        {
          "id": "C",
          "name": "選択の切り替え",
          "key": "Chart.select(繰り返し)",
          "code": "Chart.bars({ cols: DEMO.months, colW: 36, inset: 6, hi: DEMO.hi, step: DEMO.step, order: DEMO.order, color: DEMO.color, guide: { value: 30000, label: '予算' }, act: { cat: 'cat', col: 'col' } })",
          "gesture": {
            "type": "tap",
            "target": ".lgd",
            "nth": "cycle"
          },
          "loop": {
            "kind": "gesture",
            "ms": 1000
          },
          "note": "描き直さず Chart.select で切り替える。ほかのカテゴリはうすくなる。"
        }
      ]
    },
    {
      "id": "PIE",
      "name": "ドーナツグラフ",
      "category": "グラフ",
      "family": "webapp",
      "preview": "web",
      "aliases": [
        "pie",
        "chart",
        "円グラフ"
      ],
      "summary": "扇のあいだにすきまがあるドーナツ型。名前と割合は、扇から線を伸ばして外に書く。",
      "tags": [
        "統計",
        "選択"
      ],
      "usedBy": [
        "Hanbun"
      ],
      "impls": [
        {
          "platform": "web",
          "call": "Chart.pie({ items, selected, act, center, label, maskId })",
          "files": [
            "web/chart.js",
            "web/chart.css",
            "web/tokens.css"
          ],
          "usage": "円だけ動かすときは祖先に ch-anim-pie。同じ画面に2つ置くときは maskId を変える。"
        }
      ],
      "variants": [
        {
          "id": "A",
          "name": "ドーナツ(選択)",
          "key": "Chart.pie",
          "code": "Chart.pie({ items: DEMO.items, act: 'cat', center: '<span>合計</span><b>¥48,000</b>', label: 'カテゴリ別の出費' })",
          "wide": true,
          "gesture": {
            "type": "tap",
            "target": ".plab",
            "index": 0
          },
          "note": "円だけ動かすときは祖先に ch-anim-pie。同じ画面に2つ置くときは maskId を変える。"
        },
        {
          "id": "B",
          "name": "描かれる動き",
          "key": "ch-anim-pie(繰り返し)",
          "code": "Chart.pie({ items: DEMO.items, act: 'cat', center: '<span>合計</span><b>¥48,000</b>', label: 'カテゴリ別の出費', maskId: 'pm3' })",
          "wide": true,
          "loop": {
            "kind": "replay",
            "ms": 3800
          },
          "note": "時計回りに描かれ(900ms)、名前の札があとから出る。"
        },
        {
          "id": "C",
          "name": "選択の切り替え",
          "key": "Chart.select(繰り返し)",
          "code": "Chart.pie({ items: DEMO.items, act: 'cat', center: '<span>合計</span><b>¥48,000</b>', label: 'カテゴリ別の出費', maskId: 'pm4' })",
          "wide": true,
          "gesture": {
            "type": "tap",
            "target": ".plab",
            "nth": "cycle"
          },
          "loop": {
            "kind": "gesture",
            "ms": 1000
          },
          "note": "選んだ扇が、少し外へ伸びる。"
        }
      ]
    },
    {
      "id": "RNG",
      "name": "リング",
      "category": "グラフ",
      "family": "webapp",
      "preview": "none",
      "aliases": [
        "ring",
        "円",
        "進捗"
      ],
      "summary": "円のリング。value/max だけ埋まり、超過すると1周目が青、超過分が2周目に赤で伸びる。",
      "tags": [
        "進捗",
        "状態表示",
        "丸"
      ],
      "usedBy": [
        "unilife"
      ],
      "impls": [
        {
          "platform": "react",
          "call": "<Ring value max label size tone />",
          "files": [
            "react/ui.tsx"
          ],
          "usage": "size は既定 44。"
        }
      ],
      "variants": [
        {
          "id": "A",
          "name": "リング",
          "key": "tone: accent",
          "code": "<Ring value={12} max={24} label=\"取得\" />",
          "look": "細い円。進んだ分がアクセント色(動きで埋まるため、静止では描けない)"
        }
      ]
    },
    {
      "id": "SWP",
      "name": "スワイプ行",
      "category": "一覧",
      "family": "webapp",
      "preview": "web",
      "aliases": [
        "swipe",
        "スワイプ削除",
        "メールの行"
      ],
      "summary": "行を左へスワイプすると、丸いボタン(編集・削除)が出る。大きく払うと削除が自動で押される。",
      "tags": [
        "スワイプ",
        "削除",
        "一覧"
      ],
      "usedBy": [
        "Hanbun",
        "unilife"
      ],
      "impls": [
        {
          "platform": "web",
          "call": "Swipe.row(中身のHTML, id, [{act, cls, icon, text, label, full}])",
          "files": [
            "web/swipe.js",
            "web/swipe.css"
          ],
          "usage": "一度だけ Swipe.init()。クリック処理の先頭で Swipe.closeIfOpen(押された要素)。色は --swp-edit / --swp-del / --swp-bg / --swp-line / --swp-bleed。"
        },
        {
          "platform": "react",
          "call": "<SwipeRow actions={[{key, label, tone, full, onPress}]}>行の中身</SwipeRow>",
          "files": [
            "react/swipe-row.tsx",
            "react/swipe-logic.ts"
          ],
          "usage": "ボタンは角丸の四角。判断は swipe-logic.ts。"
        }
      ],
      "variants": [
        {
          "id": "A",
          "name": "スワイプでボタンが出る",
          "key": "編集 + 削除",
          "code": "Swipe.row('<div class=\"swp-in\"><div style=\"padding:1.4rem 0\">コーヒー　¥450</div></div>', 'r1', [{ act: 'edit', cls: 'edit', icon: icon('pencil'), text: '編集' }, { act: 'del', cls: 'del', icon: icon('trash'), text: '削除', full: true }])",
          "stage": "swipe",
          "react": "<SwipeRow actions={[{ key: \"edit\", … }, { key: \"del\", full: true, … }]}>行の中身</SwipeRow>",
          "gesture": {
            "type": "swipe",
            "target": ".swp-in",
            "dx": -110,
            "fx": 0.85,
            "real": true,
            "after": "swipe-close"
          },
          "loop": {
            "kind": "gesture",
            "ms": 1400
          },
          "note": "本物のスワイプ操作で、ボタンが大きくなりながら出る。離すと出たまま止まり、あとで戻る。ボタンが1つ(削除だけ)でもよい。"
        },
        {
          "id": "B",
          "name": "大きく払って削除",
          "key": "full: true",
          "code": "Swipe.row('<div class=\"swp-in\"><div style=\"padding:1.4rem 0\">ランチ　¥980</div></div>', 'r4', [{ act: 'edit', cls: 'edit', icon: icon('pencil'), text: '編集' }, { act: 'del', cls: 'del', icon: icon('trash'), text: '削除', full: true }])",
          "stage": "swipe",
          "gesture": {
            "type": "swipe",
            "target": ".swp-in",
            "dx": -270,
            "fx": 0.95,
            "real": true,
            "after": "swipe-close"
          },
          "loop": {
            "kind": "gesture",
            "ms": 1400
          },
          "note": "削除が横長に広がり、編集は薄れる。実際には、ここで削除が押される。"
        }
      ]
    },
    {
      "id": "SHT",
      "name": "シート",
      "category": "重ねる",
      "family": "webapp",
      "preview": "none",
      "aliases": [
        "sheet",
        "モーダル",
        "下から出る"
      ],
      "summary": "下から出る画面。グラバー・閉じる(×)・タイトル・完了(✓)を持つ。完了だけのシートは作らない。",
      "tags": [
        "閉じる",
        "完了",
        "確認"
      ],
      "usedBy": [
        "unilife"
      ],
      "impls": [
        {
          "platform": "react",
          "call": "<Sheet title onClose onBack action>、<FullSheet label onClose>、<SheetHeader>、SheetCloseButton / SheetBackButton / SheetActionButton / SheetGrabber",
          "files": [
            "react/ui.tsx"
          ],
          "usage": "開いたら中にフォーカスを移し、閉じたら戻す。FullSheet はスマホの全画面。"
        }
      ],
      "variants": [
        {
          "id": "A",
          "name": "シート",
          "key": "Sheet",
          "code": "<Sheet title=\"授業\" onClose={close}>…</Sheet>",
          "look": "下から出る角丸のシート。グラバー、閉じる(左)、完了(右)"
        },
        {
          "id": "B",
          "name": "全画面シート",
          "key": "FullSheet",
          "code": "<FullSheet label=\"検索\" onClose={close}>{(close) => …}</FullSheet>",
          "look": "スマホで全画面。下から上がり、下へ戻る"
        }
      ]
    },
    {
      "id": "DLG",
      "name": "確認ダイアログ",
      "category": "重ねる",
      "family": "webapp",
      "preview": "none",
      "aliases": [
        "dialog",
        "alert",
        "確認"
      ],
      "summary": "確認の画面。取り消しは左、実行は右の主ボタン。実行のボタンは動詞(削除、精算)で、削除は赤。Esc で取り消す。",
      "tags": [
        "確認",
        "削除"
      ],
      "usedBy": [
        "unilife"
      ],
      "impls": [
        {
          "platform": "react",
          "call": "<ConfirmDialog title message confirmLabel onConfirm onCancel destructive />",
          "files": [
            "react/ui.tsx"
          ],
          "usage": "実行のボタンは、はい・OK でなく動詞（削除、精算）。"
        }
      ],
      "variants": [
        {
          "id": "A",
          "name": "確認ダイアログ",
          "key": "ConfirmDialog(destructive で赤)",
          "code": "<ConfirmDialog title=\"削除\" message=\"…\" confirmLabel=\"削除\" destructive onConfirm={f} onCancel={g} />",
          "look": "文字のキャンセルと、主ボタン。削除のときは、主ボタンが赤いピルに白い文字"
        }
      ]
    },
    {
      "id": "WHL",
      "name": "ホイール選択",
      "category": "入力",
      "family": "webapp",
      "preview": "none",
      "aliases": [
        "wheel",
        "ピッカー",
        "ドラムロール"
      ],
      "summary": "ドラムロールの選択。止まったとき、中央に近い項目を選ぶ。タップ・矢印キーでも選べる。",
      "tags": [
        "選択",
        "スクロール",
        "入力"
      ],
      "usedBy": [
        "unilife"
      ],
      "impls": [
        {
          "platform": "react",
          "call": "<Wheel items value onChange label />",
          "files": [
            "react/ui.tsx"
          ],
          "usage": "止まったらバネで吸着する。"
        }
      ],
      "variants": [
        {
          "id": "A",
          "name": "ホイール",
          "key": "Wheel",
          "code": "<Wheel items={years} value={y} onChange={setY} label=\"年度\" />",
          "look": "縦にスクロールする選択肢、中央が選択"
        }
      ]
    },
    {
      "id": "EDG",
      "name": "端のぼかし",
      "category": "動き",
      "family": "webapp",
      "preview": "none",
      "aliases": [
        "blur",
        "fade",
        "スクロール端"
      ],
      "summary": "スクロールの端(上下・左右)をぼかして、内容が自然に消えるようにする。",
      "tags": [
        "スクロール",
        "ぼかし"
      ],
      "usedBy": [
        "unilife"
      ],
      "impls": [
        {
          "platform": "react",
          "call": "<FadeScroll axis tone>、<PageEdgeBlurs />、<WindowEdges />、<EdgeBlur />",
          "files": [
            "react/scroll-edge.tsx"
          ],
          "usage": "Next.js 用。PageEdgeBlurs は画面の上下端、FadeScroll は枠の中。"
        }
      ],
      "variants": [
        {
          "id": "A",
          "name": "枠の中",
          "key": "FadeScroll",
          "code": "<FadeScroll axis=\"y\" tone=\"card\">…</FadeScroll>",
          "look": "枠の端でぼかして消える"
        },
        {
          "id": "B",
          "name": "画面の端",
          "key": "PageEdgeBlurs",
          "code": "<PageEdgeBlurs />",
          "look": "画面の上端と下端のぼかし"
        }
      ]
    },
    {
      "id": "RBD",
      "name": "ゴムの弾力",
      "category": "動き",
      "family": "webapp",
      "preview": "none",
      "aliases": [
        "rubber band",
        "弾力",
        "バウンス"
      ],
      "summary": "スクロールの端でゴムのように伸びて戻る(iOS風)。作ったが、unilife では「やっぱり無し」で外した。",
      "tags": [
        "スクロール",
        "ドラッグ"
      ],
      "usedBy": [],
      "impls": [
        {
          "platform": "react",
          "call": "<RubberBand />（layout に1つ）",
          "files": [
            "react/rubber-band.ts",
            "react/rubber-band-component.tsx",
            "tests/rubber-band.test.ts"
          ],
          "usage": "固定の部品に data-bounce-pin を付ける。使うときは実機で見てから決める（K135）。"
        }
      ],
      "variants": [
        {
          "id": "A",
          "name": "iOS風",
          "key": "c=0.55",
          "code": "<RubberBand />",
          "look": "端で伸びて、離すとなめらかに戻る"
        }
      ]
    },
    {
      "id": "MOT",
      "name": "動きの決まり",
      "category": "動き",
      "family": "webapp",
      "preview": "web",
      "aliases": [
        "motion",
        "press",
        "アニメーション",
        "押し込み"
      ],
      "summary": "押してすぐ反応する動き、シートの出入り、動きの曲線の決まり。動きを減らす設定にも対応。",
      "tags": [
        "基本"
      ],
      "usedBy": [
        "unilife"
      ],
      "impls": [
        {
          "platform": "web",
          "call": "class=\"pressable\"、class=\"sheet-panel\"(+ leaving)、--ease / --ease-out / --dur-1〜3",
          "files": [
            "web/motion.css",
            "web/tokens.css"
          ],
          "usage": "tokens.css とあわせて使う(K134)。動きを減らす設定のときは止まる。"
        }
      ],
      "variants": [
        {
          "id": "A",
          "name": "押し込み",
          "key": ".pressable",
          "code": "UI.btn('primary',{},'押す',{cls:'pressable'})",
          "gesture": {
            "type": "tap",
            "target": "button.u"
          },
          "loop": {
            "kind": "gesture",
            "ms": 1400
          },
          "note": "押した瞬間は60ms、離したあとは200msで戻る。"
        },
        {
          "id": "B",
          "name": "シートが出入り",
          "key": ".sheet-panel(繰り返し)",
          "code": "'<div class=\"sheet-panel\" style=\"padding:1rem 1.75rem;border-radius:1.25rem;background:#fff;box-shadow:0 8px 24px rgba(40,32,24,.16);font-weight:600\">シート</div>'",
          "loop": {
            "kind": "sheet",
            "ms": 3000
          },
          "note": "出るのは450ms(始まりが速く、あとで減速)、閉じるのは200ms。"
        },
        {
          "id": "C",
          "name": "出る動きの曲線",
          "key": "--ease-out  450ms(繰り返し)",
          "code": "'<i style=\"display:block;width:28px;height:28px;border-radius:50%;background:#4A7CE0;transition:transform var(--dur-2) var(--ease-out)\"></i>'",
          "loop": {
            "kind": "style",
            "selector": "i",
            "prop": "transform",
            "values": [
              "translateX(0)",
              "translateX(150px)"
            ],
            "ms": 1800
          },
          "note": "始まりが速く、あとで減速する。反応が速く感じられる。"
        },
        {
          "id": "D",
          "name": "標準の動きの曲線",
          "key": "--ease  450ms(繰り返し)",
          "code": "'<i style=\"display:block;width:28px;height:28px;border-radius:50%;background:#B58AE0;transition:transform var(--dur-2) var(--ease)\"></i>'",
          "loop": {
            "kind": "style",
            "selector": "i",
            "prop": "transform",
            "values": [
              "translateX(0)",
              "translateX(150px)"
            ],
            "ms": 1800
          },
          "note": "始まりも終わりもゆるやか。画面全体の移動など、ゆっくり見せたいとき。"
        }
      ]
    },
    {
      "id": "GST",
      "name": "操作の見せ方",
      "category": "操作表現",
      "family": "webapp",
      "preview": "web",
      "aliases": [
        "gesture",
        "tap",
        "swipe",
        "click",
        "クリック",
        "タップ",
        "長押し",
        "ジェスチャー",
        "操作",
        "ポインタ",
        "カーソル",
        "マウス",
        "指",
        "丸"
      ],
      "summary": "タップ・長押し・スワイプなどの操作を、PC はマウスポインタ、スマホはタッチの丸で見せる。使い方の動画や説明の画面に使う。",
      "tags": [],
      "usedBy": [],
      "impls": [
        {
          "platform": "web",
          "call": "Gesture.play(面, { mode: 'pc' | 'touch', type, target, dx, dy, real, carry })、Gesture.target(文字)、Gesture.indicator(mode, 状態)",
          "files": [
            "web/gesture.js",
            "web/gesture.css"
          ],
          "usage": "種類は tap・doubletap・longpress・swipe・drag。tap は本物の click を送るので、スイッチなどは見本でも切り替わる。real: true なら本物の pointer イベントを送る(Swipe.row の行がそのまま反応)。PC は『マウスポインタ』、スマホは『グレーの半透明の丸』(長押しはリングが一周)。動きを減らす設定のときは reduce: true で最小にする。"
        }
      ],
      "variants": [
        {
          "id": "A",
          "name": "タップ・クリック",
          "key": "type: 'tap'",
          "code": "Gesture.target('ボタン')",
          "gesture": {
            "type": "tap",
            "target": ".gx-target",
            "click": false
          },
          "loop": {
            "kind": "gesture",
            "ms": 1200
          },
          "tags": [
            "タップ"
          ],
          "note": "丸は、触れた瞬間に少し縮み、離すと消える。ポインタは、押した位置に波紋が出る。"
        },
        {
          "id": "B",
          "name": "ダブルタップ・ダブルクリック",
          "key": "type: 'doubletap'",
          "code": "Gesture.target('項目')",
          "gesture": {
            "type": "doubletap",
            "target": ".gx-target"
          },
          "loop": {
            "kind": "gesture",
            "ms": 1200
          },
          "tags": [
            "タップ",
            "ダブルタップ"
          ],
          "note": "2回続けて押す。2回目で相手が少し弾む。"
        },
        {
          "id": "C",
          "name": "長押し",
          "key": "type: 'longpress'",
          "code": "Gesture.target('項目')",
          "gesture": {
            "type": "longpress",
            "target": ".gx-target"
          },
          "loop": {
            "kind": "gesture",
            "ms": 1200
          },
          "tags": [
            "長押し"
          ],
          "note": "丸のまわりのリングが0.9秒で一周し、押し込んだ状態で止まる。ポインタは、押し続ける。"
        },
        {
          "id": "D",
          "name": "スワイプ",
          "key": "type: 'swipe'",
          "code": "Gesture.target('カード')",
          "gesture": {
            "type": "swipe",
            "target": ".gx-target",
            "dx": -110,
            "carry": true
          },
          "loop": {
            "kind": "gesture",
            "ms": 1200
          },
          "tags": [
            "スワイプ"
          ],
          "note": "丸が横へ動き、相手も指についてくる。離すと、ゆっくり戻る。"
        },
        {
          "id": "E",
          "name": "ドラッグ",
          "key": "type: 'drag'",
          "code": "Gesture.target('項目')",
          "gesture": {
            "type": "drag",
            "target": ".gx-target",
            "dx": 110,
            "carry": true
          },
          "loop": {
            "kind": "gesture",
            "ms": 1200
          },
          "tags": [
            "ドラッグ"
          ],
          "note": "つかんでから動かす(つかむまで少し止まる)。"
        },
        {
          "id": "F",
          "name": "マウスポインタ(PC)",
          "key": "Gesture.indicator('pc', 状態)",
          "code": "Gesture.indicator('pc','')+Gesture.indicator('pc','down')",
          "tags": [
            "状態違い"
          ],
          "note": "先端が位置の基準。押している間は少し縮み、押した位置に波紋が出る。"
        },
        {
          "id": "G",
          "name": "タッチの丸(スマホ)",
          "key": "Gesture.indicator('touch', 状態)",
          "code": "Gesture.indicator('touch','')+Gesture.indicator('touch','down')+Gesture.indicator('touch','hold')",
          "stage": "scene",
          "tags": [
            "状態違い",
            "丸"
          ],
          "note": "Apple の使い方の動画のような、グレーの半透明の丸。通常・押している・長押し(リング)。中心が位置の基準。"
        }
      ]
    },
    {
      "id": "APL",
      "name": "Apple デザインシステム",
      "category": "デザイン",
      "preview": "web",
      "aliases": [
        "apple",
        "hig",
        "design system",
        "デザインシステム",
        "apple.com",
        "ios",
        "トグル",
        "セグメント",
        "ストアカード",
        "ナビ"
      ],
      "summary": "Claude Design の「Appleデザインシステム」(Apple.com風の言語)のサイト向けの部品。白・パーチメント・近黒のタイルを交互に、アクセントは Action Blue だけ、影とグラデーションなし。iOS の部品(トグル・セグメント・リスト)は、役割ごとの部品(SWT・SEG・ROW)に、iOS版として入っている。このカタログの画面も、これで作っている。",
      "tags": [
        "基本"
      ],
      "usedBy": [],
      "impls": [
        {
          "platform": "web",
          "call": "Apple.btn / Apple.link / Apple.chip / Apple.toggle / Apple.seg / Apple.tag、クラス .btn-* .chip .search .toggle .seg .store-card .tile-* .global-nav .sub-nav .list-group .sheet .t-*",
          "files": [
            "web/apple.css",
            "web/apple.js"
          ],
          "usage": "画面に <body class=\"apple\">。色は --color-*(canvas・parchment・ink・primary など)と --sys-*(iOS)、余白は --space-*(8px基準)、角丸は --radius-*。補助の文字は --color-ink-muted(元の ink-48 は4.5対1に足りない)、操作部品は44pt以上。ダークはシステムの設定に従う(元の仕様にはなく、Apple.com の暗い配色に合わせて足した)。SF Pro のフォントファイルは入れない(システムのフォント)。元: Claude Design の「Appleデザインシステム」。"
        }
      ],
      "variants": [
        {
          "id": "A",
          "name": "ボタン(4種)",
          "key": "primary / secondary / dark / pearl",
          "stage": "apple",
          "code": "Apple.btn('primary','購入')+Apple.btn('secondary','詳しく見る')+Apple.btn('dark','ストアで見る')+Apple.btn('pearl','比較')",
          "gesture": {
            "type": "tap",
            "target": ".btn",
            "index": 0
          },
          "tags": [
            "ピル",
            "塗り",
            "枠線",
            "タップ"
          ],
          "note": "押すと scale(.95)(.1秒)。ホバーの定義なし。primary と secondary はピル、dark は角丸8、pearl は角丸11と3pxの線。"
        },
        {
          "id": "B",
          "name": "ヒーローボタン",
          "key": "hero",
          "stage": "apple",
          "code": "Apple.btn('hero','今すぐ購入')",
          "gesture": {
            "type": "tap",
            "target": ".btn"
          },
          "tags": [
            "ピル",
            "塗り",
            "タップ"
          ],
          "note": "細い字(300)の大きなピル。1画面に1つ。"
        },
        {
          "id": "C",
          "name": "チップ(選択)",
          "key": ".chip",
          "stage": "apple",
          "code": "Apple.chip('食費',false)+Apple.chip('交通',true)",
          "gesture": {
            "type": "tap",
            "target": ".chip",
            "index": 0
          },
          "tags": [
            "ピル",
            "絞り込み",
            "選択",
            "状態違い",
            "タップ"
          ],
          "note": "選んだものは、青い2pxの枠と太い文字(色だけに頼らない)。"
        },
        {
          "id": "D",
          "name": "検索欄(Apple.com風)",
          "key": ".search",
          "stage": "apple",
          "code": "'<label class=\"search\" style=\"width:100%\"><span class=\"sr\">検索</span>'+icon('search')+'<input type=\"search\" placeholder=\"検索\"></label>'",
          "tags": [
            "検索",
            "入力",
            "ピル"
          ]
        },
        {
          "id": "E",
          "name": "ストアカード",
          "key": ".store-card",
          "stage": "apple",
          "code": "'<div class=\"store-card\" style=\"width:100%\"><div style=\"aspect-ratio:1.6;border-radius:var(--radius-sm);background:var(--color-parchment);margin-bottom:16px\"></div><div class=\"t-body-strong\">商品名</div><div class=\"t-body\">¥39,800</div><div style=\"margin-top:8px;color:var(--color-link)\">購入 ›</div></div>'",
          "tags": [
            "一覧",
            "入れ子"
          ],
          "note": "白、1pxの細い線、角丸18、余白24。影なし。画像の面は角丸8。"
        },
        {
          "id": "F",
          "name": "グローバルナビ",
          "key": ".global-nav",
          "stage": "apple",
          "code": "'<nav class=\"global-nav\" style=\"width:100%\"><a>Store</a><a>Mac</a><a>iPhone</a><a>サポート</a></nav>'",
          "tags": [
            "基本"
          ],
          "note": "黒の44pt帯。文字は12px。固定のナビ(.sub-nav)だけが、ぼかしを持つ。"
        }
      ],
      "family": "webapp"
    },
    {
      "id": "NET",
      "name": "通信を減らす道具",
      "category": "仕組み",
      "preview": "none",
      "aliases": [
        "net",
        "batch",
        "通信",
        "束ねる"
      ],
      "summary": "サーバーへの通信を減らす。同じ問い合わせを1回にする、一定時間は読み直さない、書き込みを束ねる。",
      "usedBy": [
        "Hanbun"
      ],
      "impls": [
        {
          "platform": "web",
          "call": "Net.once / Net.fresh / Net.batcher",
          "files": [
            "web/net.js",
            "tests/net.test.js"
          ],
          "usage": "書き込みの返事に更新後の全体を入れて、読み直しを不要にする。"
        },
        {
          "platform": "server",
          "call": "runBatch(操作の配列, 処理の一覧, { max })",
          "files": [
            "server/batch.js"
          ],
          "usage": "1つの失敗が、ほかを止めない。"
        }
      ],
      "variants": [
        {
          "id": "A",
          "name": "標準",
          "key": "Net",
          "code": "Net.fresh(load, 15)",
          "look": "見た目なし"
        }
      ],
      "tags": [
        "通信"
      ],
      "family": "system"
    },
    {
      "id": "NTF",
      "name": "ブラウザ通知",
      "category": "仕組み",
      "preview": "none",
      "aliases": [
        "notification",
        "通知"
      ],
      "summary": "ブラウザの通知。端末ごとのオン・オフ。サーバーには何も送らない。アプリが開いている間だけ届く。",
      "usedBy": [
        "unilife"
      ],
      "impls": [
        {
          "platform": "web",
          "call": "pickToNotify / toShow / showNotifications / enableBrowserNotify / disableBrowserNotify",
          "files": [
            "web/notify.ts"
          ],
          "usage": "見ている最中は出さない。常駐させる処理はアプリ全体に1つだけ（K138）。"
        }
      ],
      "variants": [
        {
          "id": "A",
          "name": "標準",
          "key": "notify",
          "code": "showNotifications(toShow(pickToNotify(list, read, pushed)))",
          "look": "見た目なし"
        }
      ],
      "tags": [
        "通知"
      ],
      "family": "system"
    },
    {
      "id": "PSH",
      "name": "プッシュ通知",
      "category": "仕組み",
      "preview": "none",
      "aliases": [
        "push",
        "プッシュ",
        "service worker"
      ],
      "summary": "閉じていても届く通知。合図だけを送り、中身は端末が決める（K139）。",
      "usedBy": [
        "unilife"
      ],
      "impls": [
        {
          "platform": "web",
          "call": "enablePush() / disablePush()、アプリの public/sw.js",
          "files": [
            "web/push-client.ts",
            "web/sw.js",
            "web/notify.ts"
          ],
          "usage": "サーバーに /api/push の GET・POST・DELETE が要る。"
        },
        {
          "platform": "server",
          "call": "cleanEndpoint / loadVapidKeys / pushTo",
          "files": [
            "server/web-push.ts"
          ],
          "usage": "宛先は正式な配信元の https だけ。外のライブラリは jose だけ。"
        }
      ],
      "variants": [
        {
          "id": "A",
          "name": "標準",
          "key": "push",
          "code": "await enablePush()",
          "look": "見た目なし"
        }
      ],
      "tags": [
        "通知",
        "安全"
      ],
      "family": "system"
    },
    {
      "id": "IMG",
      "name": "画像の情報除去",
      "category": "仕組み",
      "preview": "none",
      "aliases": [
        "exif",
        "画像",
        "メタデータ"
      ],
      "summary": "預かる画像（JPEG・PNG）から、撮影場所・機種・時刻などを取り除く。",
      "usedBy": [
        "unilife"
      ],
      "impls": [
        {
          "platform": "server",
          "call": "stripJpeg(b) / stripPng(b)（壊れた画像は ImageError）",
          "files": [
            "server/image-strip.ts"
          ],
          "usage": "端末側で描き直してから送るとさらによい。"
        }
      ],
      "variants": [
        {
          "id": "A",
          "name": "標準",
          "key": "strip",
          "code": "stripJpeg(buf)  /  stripPng(buf)",
          "look": "見た目なし"
        }
      ],
      "tags": [
        "安全"
      ],
      "family": "system"
    },
    {
      "id": "SHR",
      "name": "共有コード",
      "category": "仕組み",
      "preview": "none",
      "aliases": [
        "qr",
        "share",
        "期限つき"
      ],
      "summary": "QR・リンク用の、期限つき署名つきコード。IDそのものを入れず、期限（既定1日）が過ぎれば使えない。",
      "usedBy": [
        "unilife"
      ],
      "impls": [
        {
          "platform": "server",
          "call": "makeShareCode(secret, id, nowMs?, ttlSec?) / readShareCode(secret, code)",
          "files": [
            "server/share-code.ts"
          ],
          "usage": ""
        }
      ],
      "variants": [
        {
          "id": "A",
          "name": "標準",
          "key": "share-code",
          "code": "makeShareCode(secret, id)  /  readShareCode(secret, code)",
          "look": "見た目なし"
        }
      ],
      "tags": [
        "安全",
        "共有"
      ],
      "family": "system"
    },
    {
      "id": "CSR",
      "name": "他サイトからの書き込み防止",
      "category": "仕組み",
      "preview": "none",
      "aliases": [
        "csrf",
        "origin",
        "same-origin"
      ],
      "summary": "他のサイトからの書き込み（CSRF）を断る判定。入口（middleware）の1か所でかける。",
      "usedBy": [
        "unilife"
      ],
      "impls": [
        {
          "platform": "server",
          "call": "allowRequest(method, header) / isSameOrigin(header)",
          "files": [
            "server/same-origin.ts"
          ],
          "usage": "個別のAPIでなく、入口の1か所で。"
        }
      ],
      "variants": [
        {
          "id": "A",
          "name": "標準",
          "key": "same-origin",
          "code": "allowRequest(method, header)",
          "look": "見た目なし"
        }
      ],
      "tags": [
        "安全",
        "通信"
      ],
      "family": "system"
    }
  ],
  "tags": [
    {
      "group": "操作",
      "items": [
        "タップ",
        "ダブルタップ",
        "長押し",
        "スワイプ",
        "ドラッグ",
        "選択",
        "切り替え",
        "入力",
        "スクロール"
      ]
    },
    {
      "group": "用途",
      "items": [
        "保存",
        "削除",
        "確認",
        "閉じる",
        "完了",
        "絞り込み",
        "検索",
        "進捗",
        "統計",
        "設定",
        "通知",
        "一覧",
        "状態表示",
        "通信",
        "安全",
        "共有"
      ]
    },
    {
      "group": "見た目",
      "items": [
        "ピル",
        "ガラス",
        "丸",
        "塗り",
        "枠線",
        "文字のみ",
        "赤",
        "青",
        "色つき",
        "暗い背景",
        "積み上げ",
        "入れ子",
        "ぼかし"
      ]
    },
    {
      "group": "性質",
      "items": [
        "基本",
        "状態違い"
      ]
    }
  ],
  "derivedTags": {
    "動き": "繰り返し再生か、操作の動きを持つ",
    "繰り返し": "画面で繰り返し再生する",
    "操作見本": "タップ・スワイプなどの操作を見せる",
    "見本なし": "画面に見本を描けない(名前・コード・説明だけ)",
    "iOS": "iOS の作法のバリエーション(WebアプリとはUIの作法が違う)"
  }
}