Files
Beyond-KLE/mock/src/ui/common/KeyboardView.vue
T
ewdernaandClaude Opus 5.5 fccbc28bee mock: 仮決定に沿ってモックを実装する(PHY Editor・IDX・ELC・MAP・Generator)
provisional/decisions.md の仮決定(T-1〜T-28)に従い、mock/ に Vue 3 + Vite + TypeScript のモックを置いた。
T-3 の1〜3(回転したクラスター内のドラッグと保存の往復、IDX・MAPとの連携、ELCと出力の対応表)まで。疑似デバイスとの往復(T-24)は未着手。

- KLE JSONの入出力は、KLE本家の serial.js の deserialize の結果を期待値とする fixture(tests/fixtures/kle-oracle.json)で照合した。本家のコードは取り込んでいない(T-7)
- 基本キーコードの表は QMK の data/constants/keycodes/keycodes_0.0.1_basic.hjson(1e340cd)から抽出した
- 実装は Codex(Luna)、レビューは Codex(Sol)。検証: Vitest 53件、Playwright 20件、vue-tsc、vite build
- TypeScript は vue-tsc が 7 系に未対応のため 6 系に固定した
- 操作性の改善(ELCの提供者の表示、一括配線とGeneratorのピン設定の引き継ぎ、キー内の文字のはみ出し)は別途行う

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AATqjRyp6rrnwTNdYw8XBT
2026-09-25 22:42:30 +09:00

136 lines
3.5 KiB
Vue

<script setup lang="ts">
// 選択構成の平坦な配置を読み取り専用で描く。IDX・ELC・MAP・Generatorのパネルで共有する。
import { computed } from "vue";
import { flattenKeys } from "../../domain/geometry";
import type { FlatKey, PhyFile } from "../../domain/phy";
const props = withDefaults(
defineProps<{
phy: PhyFile;
selection: Record<string, string>;
/** キーの中央に出す文字列。省略時は先頭のラベル */
keyText?: (keyId: string) => string;
/** キーに付けるCSSクラス(状態の色分け) */
keyClass?: (keyId: string) => string | undefined;
selectedIds?: string[];
unit?: number;
}>(),
{ selectedIds: () => [], unit: 48 },
);
const emit = defineEmits<{ keyClick: [keyId: string, event: MouseEvent] }>();
const keys = computed<FlatKey[]>(() => flattenKeys(props.phy, props.selection));
// 回転を含めた四隅から表示範囲を求める
const viewBox = computed(() => {
let minX = Infinity,
minY = Infinity,
maxX = -Infinity,
maxY = -Infinity;
for (const k of keys.value) {
const r = (k.rotation * Math.PI) / 180,
c = Math.cos(r),
s = Math.sin(r);
const { w, h } = k.key.size;
for (const [lx, ly] of [
[0, 0],
[w, 0],
[0, h],
[w, h],
] as const) {
const x = k.x + lx * c - ly * s,
y = k.y + lx * s + ly * c;
minX = Math.min(minX, x);
minY = Math.min(minY, y);
maxX = Math.max(maxX, x);
maxY = Math.max(maxY, y);
}
}
if (!isFinite(minX)) return "0 0 4 2";
const pad = 0.25;
return `${minX - pad} ${minY - pad} ${maxX - minX + pad * 2} ${maxY - minY + pad * 2}`;
});
const width = computed(() => {
const parts = viewBox.value.split(" ").map(Number);
return (parts[2] ?? 4) * props.unit;
});
function text(k: FlatKey): string {
return props.keyText ? props.keyText(k.key.id) : (k.key.labels?.find((l) => l) ?? "");
}
</script>
<template>
<svg class="keyboard-view" :viewBox="viewBox" :width="width" data-testid="keyboard-view">
<g
v-for="k in keys"
:key="k.key.id"
:transform="`translate(${k.x},${k.y}) rotate(${k.rotation})`"
:class="[
'kv-key',
keyClass?.(k.key.id),
{ selected: selectedIds.includes(k.key.id), decal: k.key.decal, ghost: k.key.ghost },
]"
:data-key-id="k.key.id"
@click="emit('keyClick', k.key.id, $event)"
>
<rect x="0.03" y="0.03" :width="k.key.size.w - 0.06" :height="k.key.size.h - 0.06" rx="0.08" />
<rect
v-if="k.key.size.w2 !== undefined || k.key.size.h2 !== undefined"
:x="(k.key.size.x2 ?? 0) + 0.03"
:y="(k.key.size.y2 ?? 0) + 0.03"
:width="(k.key.size.w2 ?? k.key.size.w) - 0.06"
:height="(k.key.size.h2 ?? k.key.size.h) - 0.06"
rx="0.08"
/>
<text :x="k.key.size.w / 2" :y="k.key.size.h / 2" text-anchor="middle" dominant-baseline="central">
{{ text(k) }}
</text>
</g>
</svg>
</template>
<style scoped>
.keyboard-view {
max-width: 100%;
height: auto;
user-select: none;
}
.kv-key {
cursor: pointer;
}
.kv-key rect {
fill: #f3f4f6;
stroke: #6b7280;
stroke-width: 0.02;
}
.kv-key text {
font-size: 0.22px;
fill: #111827;
pointer-events: none;
}
.kv-key.selected rect {
stroke: #2563eb;
stroke-width: 0.06;
}
.kv-key.decal rect {
stroke-dasharray: 0.06 0.04;
fill: none;
}
.kv-key.ghost {
opacity: 0.45;
}
.kv-key.warn rect {
fill: #fde68a;
}
.kv-key.error rect {
fill: #fecaca;
}
.kv-key.ok rect {
fill: #d1fae5;
}
.kv-key.muted rect {
fill: #e5e7eb;
}
</style>