Skip to content

feat(behaviors): Step 3a — keycap behavior tiles in the Normal grid (#16)#30

Merged
numachang merged 2 commits into
feature/key-picker-rethinkfrom
feature/key-picker-step3
Jun 7, 2026
Merged

feat(behaviors): Step 3a — keycap behavior tiles in the Normal grid (#16)#30
numachang merged 2 commits into
feature/key-picker-rethinkfrom
feature/key-picker-step3

Conversation

@numachang

Copy link
Copy Markdown
Owner

Step 3a — keycap behavior tiles in the Normal grid (issue #16)

Part of the staged key-picker rethink. Base: feature/key-picker-rethink (the
integration branch — not main, not upstream). Step 3 was split into 3a/3b; this is 3a.

Plan pivot: rather than parking migrated behaviors in throwaway scaffolding until
Step 5, each step now moves its behaviors toward their final place, so the converged
tile surface is visible (and critiquable) at every step.

What's in 3a

  • tiles.tstileBindingsFor explodes a behavior into one keycap tile per
    finished (param1, param2) binding (enum/range exploded); null when a parameter
    needs a rich picker (HID key / layer), which keeps the chip + parameter-picker flow.
  • Mouse + a consolidated System tab (Bluetooth / Output / External Power / System)
    live in the Normal grid's tab strip (between International and Other). Picking a
    behavior tile sets the whole binding; picking a keycode sets the Normal key.
  • One host-adaptive "Basic" tab replaces the fixed Basic + ISO/JIS pair
    (ansi→ANSI, iso→ISO+NUHS/NUBS, jis→JIS, ko→KO). ISO-only keys appear once that
    layout is selected and stay reachable via search. One fewer tab, correct ANSI for US.
  • "+ Send with" moves from a side column to an inline row below the grid (shown on
    Basic / Function+Nav). The redundant "KEY" header is dropped and the search moves onto
    the NORMAL row
    — and now spans the behavior tiles too (reset, bt, mouse, …).
  • Slot label shows the devicetree code (&mt LSHFT RET, &mo 1, &reset, &bt BT_SEL 0) in a boxed chip, host-layout-aware (JIS Shift+2 reads ", not @).
  • Keycode-picker DRY cleanup: tab-name constants (no magic strings); consumer media
    keys promoted to Apps/Media/Special; the Other combobox grouped into labelled
    sections (other-sections.ts).

§8a before-main blocker

The "non-core binding shows a blank slot surface and silently overwrites to &kp" blocker
is largely resolved: the slot label now names the behavior + shows its code, and the
tile is highlighted. Confirm the feel before the integration branch merges to main.

Testing / gates

Intermediate PRs get no CI (ci.yml is branches:[main]), so gated locally:

  • npx tsc --noEmit — 0 errors
  • npx eslint src — 0 errors (only pre-existing warnings)
  • npx vitest run121 pass (new: tiles, formatBinding, other-sections)
  • Visual checks via Playwright across ANSI / UK / JIS layouts.

The two src/__snapshots__/*.snap files were modified before this work began and are
intentionally left out of the commit.

🤖 Generated with Claude Code

)

Tile-ify the single-action behaviors and fold them into the Normal-slot key
grid's tab strip, building toward the converged tile surface incrementally.

- tiles.ts: tileBindingsFor explodes a behavior into one keycap tile per
  finished (param1,param2) binding; null when a param needs a rich picker.
- Mouse + a consolidated System tab (Bluetooth/Output/External Power/System)
  live in the Normal grid's tab strip between International and Other; picking a
  behavior tile sets the whole binding (HidUsagePicker extraTabs/onSelectBinding).
- Collapse the two basic-tier tabs into one host-adaptive "Basic" tab
  (ansi->ANSI, iso->ISO+NUHS/NUBS, jis->JIS, ko->KO); ISO keys appear once that
  layout is selected and stay searchable.
- "+ Send with" moves from a side column to an inline row below the grid
  (Basic / Function+Nav only); drop the "KEY" header and host the search on the
  NORMAL row; the filter now spans the behavior tiles too.
- Slot label shows the devicetree code (formatBinding: &mt LSHFT RET, &mo 1,
  &reset, ...) in a boxed chip, host-layout-aware (JIS Shift+2 = ").
- Keycode-picker DRY cleanup: tab-name constants; promote consumer media keys to
  Apps/Media/Special; group the Other combobox into labelled sections
  (other-sections.ts).

Tests: tiles / formatBinding / other-sections (121 pass). Snapshot files left out.

Co-Authored-By: Claude <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 7, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
zmk-studio-tweaks 2076357 Commit Preview URL

Branch Preview URL
Jun 07 2026, 02:52 AM

@numachang numachang left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

レビュー — Step 3a: Normal グリッドへの behavior タイル統合

完成度が高く、そのままマージしてよい品質です。設計ドキュメント・コミットメッセージ・型・テストが揃っており、純ロジック(tiles.ts / other-sections.ts / formatBinding)を React から切り離してユニットテスト化している点が特に良いです。base も feature/key-picker-rethink で正しく、フォークの最小差分方針に沿っています。

ローカル検証

  • npx vitest run(新規3ファイル)→ 26 pass
  • npx tsc --noEmit → 0 error
  • npx eslint(変更6ファイル)→ 0 error / 既存 warning 2件のみ
  • ChevronDown は Other コンボボックスで継続使用(デッドインポートなし)、collapsible/modsOpen/activeModSummary 等の旧モディファイア機構の残骸なし

良い点

  • host 連動 Basic タブ統合で、ANSI ユーザーに ISO 形状(NUHS/NUBS)が出ていた挙動を解消しタブ数も削減。
  • enumerateParamhidUsage/layerIdnull を返し behavior 全体を非タイル化する分岐がテストで網羅。
  • formatBinding が既存シリアライザを再利用し constant 名を優先。&bt BT_SEL 2 / &bt BT_CLR まで検証済み。
  • 検索が behavior タイルも横断(reset/bt/mouse)し、以前の「検索がキーコード専用」の穴を解消。

結論: Approve 相当。以下の指摘はすべて Low / 任意対応でブロッカーなし。§8a ブロッカー(非コア binding の空スロット面・黙った &kp 上書き)も実質解消されています。本文どおり main へ統合する前に実機で触り心地を最終確認すれば十分です。

(インラインに Low の指摘を4点付けています)

Comment thread src/behaviors/parameters.ts Outdated
if (sets.length === 0) return ref;

const set =
sets.find((s) => validateValue(layerIds, binding.param1, s.param1)) ?? sets[0];

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Low / nit] フォールバック ?? sets[0]:param1 がどの set にもマッチしないとき先頭 set を使うため、param2 を持つ set が選ばれると実体と異なる余分なパラメータを表示し得ます。タイル/コア由来の binding は必ず有効なので到達は稀ですが、「マッチしないなら ref だけ返す」方が安全側です。

Comment thread src/behaviors/HidUsagePicker.tsx Outdated
label={tile.label}
>
<Button
aria-pressed={active}

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Low / a11y] タイル選択のセマンティクスが2箇所で不一致です。ここは role="group" + aria-pressed(トグル意味)ですが、BehaviorBindingPicker の残置領域は radiogroup + role="radio"。「現在の binding を1つ選ぶ」単一選択なので、両方を radio 系に揃える方が支援技術に正確に伝わります(aria-pressed は再クリックで解除できる含みがあるが実際は解除不可)。

// so it only fires when the binding actually changes — otherwise it would snap
// the tab back to Mouse/System the moment the user clicks a keycode tab to
// pick a Normal key.
useEffect(() => {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[情報 / 変更不要] この activeBinding ベースのタブジャンプと、上の value ベースの setActiveTab(null) が同一 render で発火し、最終状態は「後に宣言されたこちらが勝つ」前提で正しく動きます。コメントで意図は説明済みですが、宣言順依存でやや脆い構造である点だけ共有まで。

setActiveTier(currentClass.tier);
setActiveGroup(currentClass.group);
if (currentGroup) setActiveGroup(currentGroup);
}, [behaviorId]);

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Low / nit] exhaustive-deps warning が残ります(currentGroup 欠落)。behaviorId 変化時のみ追従させる意図だと思いますが、他箇所と同様 // eslint-disable-next-line react-hooks/exhaustive-deps を付けると warning が消えて意図も明示できます。329 行目の effect も同様。

- formatBinding: when param1 matches no metadata set, emit just the behavior
  reference rather than borrowing the first set's params (avoids showing a
  param2 that doesn't apply). [review #1]
- Behavior tiles are single-select, so make them `role="radio"` inside a
  `radiogroup` (consistent with the residual behavior chips) instead of
  `aria-pressed` toggles; native button + `title` keeps the visible label and
  drops the now-redundant tooltip. Search-result tiles get their own
  radiogroup. [review #2]
- Annotate the two intentional exhaustive-deps effects with
  `eslint-disable-next-line` so the warnings clear and the intent is explicit.
  [review #4]

Review #3 (declaration-order of the two tab-select effects) was info-only / no
change requested; left as-is.

Co-Authored-By: Claude <noreply@anthropic.com>
@numachang

Copy link
Copy Markdown
Owner Author

レビューありがとうございます。Low 指摘4件を 2076357 で対応しました。

  • feat: Reliable keymap import/export (refines upstream PR #171) #1 (parameters.ts:90 — formatBinding fallback): マッチする set が無いときは先頭 set を流用せず ref だけ返すようにしました(適用されない param2 を表示するリスクを排除)。
  • Triple-defined key naming: pick a SSOT and derive labels from it #2 (HidUsagePicker:709 — a11y): behavior タイルを単一選択として role="radio" + radiogroup に統一(残置チップと一致)。aria-pressed の「解除できる含み」問題も解消。Tooltip 依存の都合で native <button> 化し、ラベルは可視なので title 属性に置換。検索結果のタイルも専用 radiogroup に。Playwright で role/aria-checked/クリックでの binding 設定・検索からの選択を確認済み。
  • Add hover tooltips for behaviors and category labels #3 (HidUsagePicker:570 — 宣言順依存): 「変更不要」とのことなので据え置き。意図はコメントで明示済みです。
  • feat(picker): Visual HID/behavior picker built on upstream PR #159 #4 (BehaviorBindingPicker:239/329 — exhaustive-deps): 2つの effect に eslint-disable-next-line を付与し warning 解消+意図明示。--report-unused-disable-directives でも unused 無しを確認。

ローカル gate: tsc --noEmit 0 error / npm run lint 0 error / vitest run 121 pass。

§8a の実機タッチ最終確認のうえ、feature/key-picker-rethink へマージで問題なければ進めます。

@numachang numachang merged commit c858604 into feature/key-picker-rethink Jun 7, 2026
1 check passed
@numachang numachang deleted the feature/key-picker-step3 branch June 7, 2026 04:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant