Skip to content

Commit 7706c3c

Browse files
committed
feat: design custom keyboard layouts
1 parent 2597a88 commit 7706c3c

22 files changed

+645
-7
lines changed

package-lock.json

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/keybr-keyboard-io/lib/layoutbuilder.ts

+13-7
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
type KeyId,
66
KeyModifier,
77
} from "@keybr/keyboard";
8+
import { isDiacritic } from "@keybr/unicode";
89
import { type CharacterList, type KeyMap } from "./json.ts";
910
import { characterKeys } from "./keys.ts";
1011

@@ -120,13 +121,18 @@ export class LayoutBuilder implements Iterable<KeyCharacters> {
120121
}
121122

122123
function fix(character: Character | null): Character | null {
123-
switch (character) {
124-
case /* ZERO WIDTH NON-JOINER */ 0x200c:
125-
case /* ZERO WIDTH JOINER */ 0x200d:
126-
case /* LEFT-TO-RIGHT MARK */ 0x200e:
127-
case /* RIGHT-TO-LEFT MARK */ 0x200f:
128-
case /* COMBINING GRAPHEME JOINER */ 0x034f:
129-
return { special: character };
124+
if (KeyCharacters.isCodePoint(character)) {
125+
if (isDiacritic(character)) {
126+
return { dead: character };
127+
}
128+
switch (character) {
129+
case /* ZERO WIDTH NON-JOINER */ 0x200c:
130+
case /* ZERO WIDTH JOINER */ 0x200d:
131+
case /* LEFT-TO-RIGHT MARK */ 0x200e:
132+
case /* RIGHT-TO-LEFT MARK */ 0x200f:
133+
case /* COMBINING GRAPHEME JOINER */ 0x034f:
134+
return { special: character };
135+
}
130136
}
131137
return character;
132138
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
.char {
2+
display: inline-block;
3+
inline-size: 2rem;
4+
border: 1px solid var(--primary-d2);
5+
border-radius: 0.5rem;
6+
background-color: var(--primary-d1);
7+
font-weight: bold;
8+
text-align: center;
9+
}
10+
11+
.dead {
12+
color: var(--KeyboardKey-symbol--dead__color);
13+
}
14+
15+
.ligature {
16+
color: var(--KeyboardKey-symbol--ligature__color);
17+
}
18+
19+
.unassigned {
20+
color: var(--text-color-f2);
21+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { type Character, KeyCharacters } from "@keybr/keyboard";
2+
import { formatCodePoint, isDiacritic } from "@keybr/unicode";
3+
import { clsx } from "clsx";
4+
import * as styles from "./CharacterInfo.module.less";
5+
6+
export function CharacterInfo({
7+
character,
8+
}: {
9+
readonly character: Character | null;
10+
}) {
11+
if (character == null || character === 0x0000) {
12+
return <em className={styles.unassigned}>Unassigned</em>;
13+
}
14+
if (KeyCharacters.isCodePoint(character)) {
15+
let label;
16+
switch (character) {
17+
case 0x0020:
18+
label = "SPACE";
19+
break;
20+
case 0x00a0:
21+
label = "NBSP";
22+
break;
23+
default:
24+
label = (
25+
<span className={styles.char}>{String.fromCodePoint(character)}</span>
26+
);
27+
break;
28+
}
29+
return (
30+
<>
31+
{label} {formatCodePoint(character)}
32+
</>
33+
);
34+
}
35+
if (KeyCharacters.isDead(character)) {
36+
const { dead } = character;
37+
const label = (
38+
<span className={clsx(styles.char, styles.dead)}>
39+
{isDiacritic(dead)
40+
? String.fromCodePoint(/* DOTTED CIRCLE */ 0x25cc, dead)
41+
: String.fromCodePoint(dead)}
42+
</span>
43+
);
44+
return (
45+
<>
46+
{label} {formatCodePoint(dead)}
47+
</>
48+
);
49+
}
50+
if (KeyCharacters.isLigature(character)) {
51+
return (
52+
<em className={clsx(styles.char, styles.ligature)}>
53+
{character.ligature}
54+
</em>
55+
);
56+
}
57+
if (KeyCharacters.isSpecial(character)) {
58+
return formatCodePoint(character.special);
59+
}
60+
throw new TypeError();
61+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { type KeyId } from "@keybr/keyboard";
2+
import { exportLayout, importLayout, LayoutBuilder } from "@keybr/keyboard-io";
3+
import { Button, ErrorAlert, Field, FieldList } from "@keybr/widget";
4+
import { useRef, useState } from "react";
5+
import { CustomLayoutProvider, useCustomLayout } from "./context.tsx";
6+
import { LayoutView } from "./LayoutView.tsx";
7+
import { LiveImport } from "./LiveImport.tsx";
8+
9+
export function CustomLayoutDesigner() {
10+
return (
11+
<CustomLayoutProvider>
12+
<DesignPane />
13+
</CustomLayoutProvider>
14+
);
15+
}
16+
17+
function DesignPane() {
18+
const exportRef = useRef<HTMLAnchorElement>(null);
19+
const importRef = useRef<HTMLInputElement>(null);
20+
const { layout, setLayout } = useCustomLayout();
21+
const [keyId, setKeyId] = useState<KeyId>("Space");
22+
return (
23+
<section>
24+
<a
25+
ref={exportRef}
26+
href="#"
27+
download="layout.json"
28+
hidden={true}
29+
style={{ inlineSize: 0, blockSize: 0, overflow: "hidden" }}
30+
/>
31+
<input
32+
ref={importRef}
33+
type="file"
34+
accept="*/*"
35+
hidden={true}
36+
style={{ inlineSize: 0, blockSize: 0, overflow: "hidden" }}
37+
onChange={() => {
38+
const el = importRef.current!;
39+
const files = el.files;
40+
if (files != null && files.length > 0) {
41+
importLayout(files[0])
42+
.then((result) => {
43+
if (result == null) {
44+
ErrorAlert.report("Invalid layout file");
45+
} else {
46+
const { layout, warnings } = result;
47+
setLayout(layout);
48+
if (warnings.length > 0) {
49+
ErrorAlert.report(new AggregateError(warnings));
50+
}
51+
}
52+
})
53+
.catch((err) => {
54+
console.error(err);
55+
})
56+
.finally(() => {
57+
el.value = "";
58+
});
59+
}
60+
}}
61+
/>
62+
<LayoutView keyId={keyId} setKeyId={setKeyId} />
63+
<LiveImport onChange={setKeyId} />
64+
<FieldList>
65+
<Field>
66+
<Button
67+
label="Export"
68+
onClick={() => {
69+
exportLayout(layout)
70+
.then((blob) => {
71+
const el = exportRef.current!;
72+
el.setAttribute("href", URL.createObjectURL(blob));
73+
el.click();
74+
})
75+
.catch((err) => {
76+
console.error(err);
77+
});
78+
}}
79+
/>
80+
</Field>
81+
<Field>
82+
<Button
83+
label="Import"
84+
onClick={() => {
85+
const el = importRef.current!;
86+
el.click();
87+
}}
88+
/>
89+
</Field>
90+
<Field.Filler />
91+
<Field>
92+
<Button
93+
label="Reset"
94+
onClick={() => {
95+
setLayout(new LayoutBuilder());
96+
}}
97+
/>
98+
</Field>
99+
</FieldList>
100+
</section>
101+
);
102+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { type KeyCharacters, KeyModifier } from "@keybr/keyboard";
2+
import { Field, FieldList, Name, Value } from "@keybr/widget";
3+
import { CharacterInfo } from "./CharacterInfo.tsx";
4+
import { ModifierInfo } from "./ModifierInfo.tsx";
5+
6+
export function KeyDetails({
7+
characters: { id, a, b, c, d },
8+
}: {
9+
readonly characters: KeyCharacters;
10+
}) {
11+
return (
12+
<>
13+
<FieldList>
14+
<Field size={6}>Id</Field>
15+
<Field>
16+
<Value>{id}</Value>
17+
</Field>
18+
</FieldList>
19+
<FieldList>
20+
<Field size={6}>
21+
<Name>
22+
<ModifierInfo modifier={KeyModifier.None} />
23+
</Name>
24+
</Field>
25+
<Field size={10}>
26+
<Value>
27+
<CharacterInfo character={a} />
28+
</Value>
29+
</Field>
30+
<Field size={6}>
31+
<Name>
32+
<ModifierInfo modifier={KeyModifier.Shift} />
33+
</Name>
34+
</Field>
35+
<Field size={10}>
36+
<Value>
37+
<CharacterInfo character={b} />
38+
</Value>
39+
</Field>
40+
</FieldList>
41+
<FieldList>
42+
<Field size={6}>
43+
<Name>
44+
<ModifierInfo modifier={KeyModifier.Alt} />
45+
</Name>
46+
</Field>
47+
<Field size={10}>
48+
<Value>
49+
<CharacterInfo character={c} />
50+
</Value>
51+
</Field>
52+
<Field size={6}>
53+
<Name>
54+
<ModifierInfo modifier={KeyModifier.ShiftAlt} />
55+
</Name>
56+
</Field>
57+
<Field size={10}>
58+
<Value>
59+
<CharacterInfo character={d} />
60+
</Value>
61+
</Field>
62+
</FieldList>
63+
</>
64+
);
65+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { TextField } from "@keybr/widget";
2+
import { useCustomLayout } from "./context.tsx";
3+
4+
export function LayoutJson() {
5+
const { layout } = useCustomLayout();
6+
return (
7+
<TextField
8+
type="textarea"
9+
value={JSON.stringify(layout.toJSON(), null, 2)}
10+
size="full"
11+
disabled={true}
12+
/>
13+
);
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
.table {
2+
table-layout: fixed;
3+
inline-size: 100%;
4+
margin-block: 1rem;
5+
padding: 1rem;
6+
border: var(--separator-border);
7+
border-collapse: collapse;
8+
}
9+
10+
.row {
11+
border-block-end: var(--separator-border);
12+
}
13+
14+
.keyCol {
15+
padding-inline: 1rem;
16+
text-align: start;
17+
}
18+
19+
.characterCol {
20+
inline-size: 20%;
21+
padding-inline: 1rem;
22+
text-align: start;
23+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { KeyCharacters, KeyModifier } from "@keybr/keyboard";
2+
import { LayoutBuilder } from "@keybr/keyboard-io";
3+
import { CharacterInfo } from "./CharacterInfo.tsx";
4+
import { useCustomLayout } from "./context.tsx";
5+
import * as styles from "./LayoutTable.module.less";
6+
import { ModifierInfo } from "./ModifierInfo.tsx";
7+
8+
export function LayoutTable() {
9+
const { layout } = useCustomLayout();
10+
return (
11+
<table className={styles.table}>
12+
<thead>
13+
<tr className={styles.row}>
14+
<th className={styles.keyCol}>Key</th>
15+
<th className={styles.characterCol}>
16+
<ModifierInfo modifier={KeyModifier.None} />
17+
</th>
18+
<th className={styles.characterCol}>
19+
<ModifierInfo modifier={KeyModifier.Shift} />
20+
</th>
21+
<th className={styles.characterCol}>
22+
<ModifierInfo modifier={KeyModifier.Alt} />
23+
</th>
24+
<th className={styles.characterCol}>
25+
<ModifierInfo modifier={KeyModifier.ShiftAlt} />
26+
</th>
27+
</tr>
28+
</thead>
29+
<tbody>
30+
{LayoutBuilder.allKeys().map((id) => {
31+
const { a, b, c, d } =
32+
layout.get(id) ?? new KeyCharacters(id, null, null, null, null);
33+
return (
34+
<tr key={id} className={styles.row}>
35+
<td className={styles.keyCol}>{id}</td>
36+
<td className={styles.characterCol}>
37+
<CharacterInfo character={a} />
38+
</td>
39+
<td className={styles.characterCol}>
40+
<CharacterInfo character={b} />
41+
</td>
42+
<td className={styles.characterCol}>
43+
<CharacterInfo character={c} />
44+
</td>
45+
<td className={styles.characterCol}>
46+
<CharacterInfo character={d} />
47+
</td>
48+
</tr>
49+
);
50+
})}
51+
</tbody>
52+
</table>
53+
);
54+
}

0 commit comments

Comments
 (0)