Skip to content

Commit 177f2df

Browse files
committed
feat: Tons of layout fixes, device selection.
* Tweaks to full app layout. * Add device selection to connect modal.
1 parent ad0373c commit 177f2df

File tree

12 files changed

+247
-141
lines changed

12 files changed

+247
-141
lines changed

src/App.css

Lines changed: 8 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,12 @@
11
#root {
2-
max-width: 1280px;
3-
margin: 0 auto;
4-
padding: 2rem;
5-
text-align: center;
2+
margin: 0 0;
3+
width: 100%;
4+
height: 100vh;
65
}
76

8-
.logo {
9-
height: 6em;
10-
padding: 1.5em;
11-
will-change: filter;
12-
transition: filter 300ms;
13-
}
14-
.logo:hover {
15-
filter: drop-shadow(0 0 2em #646cffaa);
16-
}
17-
.logo.react:hover {
18-
filter: drop-shadow(0 0 2em #61dafbaa);
19-
}
20-
21-
@keyframes logo-spin {
22-
from {
23-
transform: rotate(0deg);
24-
}
25-
to {
26-
transform: rotate(360deg);
27-
}
28-
}
29-
30-
@media (prefers-reduced-motion: no-preference) {
31-
a:nth-of-type(2) .logo {
32-
animation: logo-spin infinite 20s linear;
33-
}
34-
}
35-
36-
.card {
37-
padding: 2em;
38-
}
39-
40-
.read-the-docs {
41-
color: #888;
7+
.zmk-app {
8+
display: grid;
9+
grid-template-columns: auto;
10+
grid-template-rows: auto 1fr;
11+
height: 100%;
4212
}

src/App.tsx

Lines changed: 62 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,70 @@
11
import './App.css'
2-
import RpcTest from './RpcTest';
2+
import { AppHeader } from './AppHeader';
3+
4+
import { create_rpc_connection, RpcConnection } from "ts-zmk-rpc-core";
5+
import { ConnectionContext } from './rpc/ConnectionContext';
6+
import React, { useState } from 'react';
7+
import { ConnectModal, TransportFactory } from './ConnectModal';
8+
9+
import type { RpcTransport } from "ts-zmk-rpc-core/transport/index";
10+
import { connect as gatt_connect } from "ts-zmk-rpc-core/transport/gatt";
11+
import { connect as serial_connect } from "ts-zmk-rpc-core/transport/serial";
12+
import { connect as tauri_ble_connect, list_devices as ble_list_devices } from './tauri/ble';
13+
import { connect as tauri_serial_connect, list_devices as serial_list_devices } from './tauri/serial';
14+
import Keyboard from './keyboard/Keyboard';
15+
16+
declare global {
17+
interface Window { __TAURI_INTERNALS__?: object; }
18+
}
19+
20+
const TRANSPORTS: TransportFactory[] = [
21+
navigator.bluetooth && { label: "BLE", connect: gatt_connect },
22+
navigator.serial && { label: "Serial", connect: serial_connect },
23+
... window.__TAURI_INTERNALS__ ? [{ label: "BLE", pick_and_connect: {connect: tauri_ble_connect, list: ble_list_devices } }] : [],
24+
... window.__TAURI_INTERNALS__ ? [{ label: "Serial", pick_and_connect: { connect: tauri_serial_connect, list: serial_list_devices }}] : [],
25+
].filter((t) => t !== undefined);
26+
27+
async function listen_for_notifications(notification_stream: ReadableStream<Notification>): Promise<void> {
28+
let reader = notification_stream.getReader();
29+
do {
30+
try {
31+
let { done, value } = await reader.read();
32+
if (done) {
33+
break;
34+
}
35+
36+
// TODO: Do something with the notifications
37+
console.log("Notification", value);
38+
} catch (e) {
39+
reader.releaseLock();
40+
throw e;
41+
}
42+
} while (true)
43+
44+
reader.releaseLock();
45+
}
46+
47+
async function connect(transport: RpcTransport, setConn: React.Dispatch<RpcConnection | null>) {
48+
let rpc_conn = await create_rpc_connection(transport);
49+
50+
listen_for_notifications(rpc_conn.notification_readable).then(() => {
51+
setConn(null);
52+
});
53+
54+
setConn(rpc_conn);
55+
}
356

457
function App() {
58+
const [conn, setConn] = useState<RpcConnection | null>(null);
59+
560
return (
6-
<>
7-
<h1>ZMK Studio Proof Of Concept</h1>
8-
<div className="card">
9-
<RpcTest />
61+
<ConnectionContext.Provider value={conn}>
62+
<ConnectModal open={!conn} transports={TRANSPORTS} onTransportCreated={(t) => connect(t, setConn)} />
63+
<div className='zmk-app'>
64+
<AppHeader connectedDeviceLabel={conn?.label} />
65+
<Keyboard />
1066
</div>
11-
</>
67+
</ConnectionContext.Provider>
1268
)
1369
}
1470

src/AppHeader.stories.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import type { Meta, StoryObj } from '@storybook/react';
2+
import { fn } from '@storybook/test';
3+
import { AppHeader } from './AppHeader';
4+
5+
// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
6+
const meta = {
7+
title: 'Application/AppHeader',
8+
component: AppHeader,
9+
parameters: {
10+
// Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
11+
layout: 'centered',
12+
},
13+
// This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
14+
tags: ['autodocs'],
15+
// More on argTypes: https://storybook.js.org/docs/api/argtypes
16+
argTypes: {
17+
// backgroundColor: { control: 'color' },
18+
},
19+
// Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args
20+
args: { },
21+
} satisfies Meta<typeof AppHeader>;
22+
23+
export default meta;
24+
type Story = StoryObj<typeof meta>;
25+
26+
export const Standard: Story = {
27+
args: {
28+
},
29+
};

src/AppHeader.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import './app-header.css';
2+
3+
export interface AppHeaderProps {
4+
connectedDeviceLabel?: string;
5+
}
6+
7+
export const AppHeader = ({connectedDeviceLabel}: AppHeaderProps) => {
8+
return (
9+
<header className="zmk-app-header">
10+
<p className='zmk-app-header__product-label'>ZMK Studio</p>
11+
<p className='zmk-app-header__connected-device'>{connectedDeviceLabel}</p>
12+
<div><p>Controls</p></div>
13+
</header>
14+
);
15+
}

src/ConnectModal.tsx

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import React, { useEffect, useRef, useState } from "react";
2+
3+
import type { RpcTransport } from "ts-zmk-rpc-core/transport/index";
4+
import type { AvailableDevice } from './tauri/index';
5+
6+
export type TransportFactory = { label: string, connect?: () => Promise<RpcTransport>, pick_and_connect?: { list: () => Promise<Array<AvailableDevice>>, connect: (dev: AvailableDevice) => Promise<RpcTransport> }};
7+
8+
export interface ConnectModalProps {
9+
open?: boolean;
10+
transports: TransportFactory[];
11+
onTransportCreated: (t: RpcTransport) => void;
12+
}
13+
14+
async function transportClicked(t: RpcTransport, setAvailableDevices: React.Dispatch<AvailableDevice[] | null>, onTransportCreated: (t: RpcTransport) => void) {
15+
if (t.pick_and_connect) {
16+
setAvailableDevices(await t.pick_and_connect.list());
17+
} else {
18+
onTransportCreated(await t.connect());
19+
}
20+
}
21+
22+
export const ConnectModal = ({ open, transports, onTransportCreated }: ConnectModalProps) => {
23+
const dialog = useRef<HTMLDialogElement | null>(null);
24+
const [availableDevices, setAvailableDevices] = useState<AvailableDevice[] | null>(null);
25+
const [selectedTransport, setSelectedTransport] = useState<RpcTransport | null>(null);
26+
27+
useEffect(() => {
28+
if (!selectedTransport) {
29+
setAvailableDevices(null);
30+
return;
31+
}
32+
33+
let ignore = false;
34+
35+
if (selectedTransport.connect) {
36+
async function connectTransport() {
37+
const transport = await selectedTransport.connect();
38+
39+
if (!ignore) {
40+
onTransportCreated(transport);
41+
setSelectedTransport(null);
42+
}
43+
}
44+
45+
connectTransport();
46+
} else {
47+
async function loadAvailableDevices() {
48+
const devices = await selectedTransport.pick_and_connect.list();
49+
50+
if (!ignore) {
51+
setAvailableDevices(devices);
52+
}
53+
}
54+
55+
loadAvailableDevices();
56+
}
57+
58+
59+
return () => { ignore = true };
60+
}, [selectedTransport]);
61+
62+
let connections = transports.map((t) => <button key={t.label} onClick={async () => setSelectedTransport(t)}>{t.label}</button>);
63+
64+
useEffect(() => {
65+
if (dialog.current) {
66+
if (open) {
67+
if (!dialog.current.open) {
68+
dialog.current.showModal();
69+
}
70+
} else {
71+
dialog.current.close();
72+
}
73+
}
74+
}, [open]);
75+
76+
return (
77+
<dialog ref={dialog}>
78+
<h1>Welcome to ZMK Studio</h1>
79+
<p>Select a connection type:</p>
80+
<ul>{connections}</ul>
81+
{selectedTransport && availableDevices && (<ul>{availableDevices.map((d) => <li key={d.id} onClick={async () => {onTransportCreated(await selectedTransport.pick_and_connect.connect(d)); setSelectedTransport(null);}}>{d.label}</li>)}</ul>)}
82+
</dialog>
83+
);
84+
}

src/RpcTest.tsx

Lines changed: 0 additions & 87 deletions
This file was deleted.

src/app-header.css

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
2+
.zmk-app-header {
3+
/* position: fixed; */
4+
top: 0;
5+
left: 0;
6+
right: 0;
7+
padding: 0 1em 0 1em;
8+
display: grid;
9+
grid-template-columns: auto 1fr auto;
10+
grid-template-rows: auto;
11+
column-gap: 1em;
12+
align-content: space-between;
13+
justify-content: center;
14+
15+
border-bottom: 1px solid light-dark(black, white);
16+
}
17+
18+
.zmk-app-header__connected-device {
19+
text-align: center;
20+
}

src/index.css

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ a:hover {
2424

2525
body {
2626
margin: 0;
27-
display: flex;
28-
place-items: center;
27+
/* display: flex; */
28+
/* place-items: center; */
2929
min-width: 320px;
3030
min-height: 100vh;
3131
}

0 commit comments

Comments
 (0)