Spaces:
Running
Running
RemiFabre commited on
Commit ·
4f5e7fd
1
Parent(s): d96c9a7
ui: compact, denser layout so message + transcript fit together
Browse files- Message box is now a single line (was 3-row textarea); long text scrolls.
- Removed the Speed control from Compose (lives in Settings; brisk was
unreliable). Kept 'Who sends it?' and the Morse preview.
- Tighter spacing everywhere: smaller uppercase section headers, reduced
gaps/paddings, shorter scope, height-capped scrolling transcript.
- Fix el() parser: '.class#id' (class-before-id) order silently failed, so the
three view roots never got their .view class. Now any order works.
- lib/version.js +1 -1
- style.css +28 -29
- tools/__pycache__/calibrate_audio.cpython-312.pyc +0 -0
- tools/__pycache__/robot_clap_test.cpython-312.pyc +0 -0
- views/compose.js +2 -17
- views/dom.js +12 -5
- views/listen.js +0 -1
lib/version.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
// Deployment version label, shown in Settings so you can verify which build
|
| 2 |
// is actually loaded after a push. Format: "YYYY-MM-DD rN", bumped on push.
|
| 3 |
-
export const APP_VERSION = "2026-06-01
|
|
|
|
| 1 |
// Deployment version label, shown in Settings so you can verify which build
|
| 2 |
// is actually loaded after a push. Format: "YYYY-MM-DD rN", bumped on push.
|
| 3 |
+
export const APP_VERSION = "2026-06-01 r5 compact layout";
|
style.css
CHANGED
|
@@ -60,8 +60,8 @@ html, body {
|
|
| 60 |
display: flex;
|
| 61 |
align-items: center;
|
| 62 |
justify-content: space-between;
|
| 63 |
-
padding:
|
| 64 |
-
padding-top: max(
|
| 65 |
background: color-mix(in srgb, var(--bg) 88%, transparent);
|
| 66 |
backdrop-filter: blur(12px);
|
| 67 |
border-bottom: 1px solid var(--line);
|
|
@@ -82,11 +82,11 @@ html, body {
|
|
| 82 |
.icon-btn:active { transform: scale(0.95); }
|
| 83 |
|
| 84 |
/* ─── Scrolling single page ───────────────────────────────── */
|
| 85 |
-
.scroller { flex: 1; padding:
|
| 86 |
-
.divider { width: 100%; border: 0; border-top: 1px solid var(--line); margin:
|
| 87 |
.view-host { flex: 1; padding: 16px 16px 96px; }
|
| 88 |
-
.view { display: flex; flex-direction: column; gap:
|
| 89 |
-
.view h2 { margin:
|
| 90 |
.view h3 { margin: 6px 0; font-size: 15px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.08em; }
|
| 91 |
.muted { color: var(--muted); }
|
| 92 |
|
|
@@ -123,7 +123,7 @@ html, body {
|
|
| 123 |
.tab.active { color: var(--accent); background: color-mix(in srgb, var(--accent) 12%, transparent); }
|
| 124 |
|
| 125 |
/* ─── Fields & controls ───────────────────────────────────── */
|
| 126 |
-
.field { display: flex; flex-direction: column; gap:
|
| 127 |
.field-label { font-size: 13px; font-weight: 600; color: var(--muted); }
|
| 128 |
.row { display: flex; gap: 10px; align-items: center; }
|
| 129 |
.row.between { justify-content: space-between; }
|
|
@@ -131,15 +131,13 @@ html, body {
|
|
| 131 |
|
| 132 |
.compose-input {
|
| 133 |
width: 100%;
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
padding: 14px;
|
| 137 |
-
font-size: 18px;
|
| 138 |
font-family: inherit;
|
| 139 |
color: var(--text);
|
| 140 |
background: var(--panel);
|
| 141 |
border: 1px solid var(--line);
|
| 142 |
-
border-radius:
|
| 143 |
outline: none;
|
| 144 |
}
|
| 145 |
.compose-input:focus { border-color: var(--accent); }
|
|
@@ -162,15 +160,15 @@ html, body {
|
|
| 162 |
.btn.big { flex: 1; padding: 16px; font-size: 17px; }
|
| 163 |
.btn.listening { background: linear-gradient(135deg, var(--danger), #ff9a5a); }
|
| 164 |
|
| 165 |
-
.segmented { display: flex; gap: 6px; background: var(--panel); border: 1px solid var(--line); border-radius:
|
| 166 |
.seg {
|
| 167 |
flex: 1;
|
| 168 |
appearance: none;
|
| 169 |
background: transparent;
|
| 170 |
border: 0;
|
| 171 |
color: var(--muted);
|
| 172 |
-
padding:
|
| 173 |
-
border-radius:
|
| 174 |
font-size: 14px;
|
| 175 |
font-weight: 600;
|
| 176 |
cursor: pointer;
|
|
@@ -180,20 +178,21 @@ html, body {
|
|
| 180 |
.range { width: 100%; accent-color: var(--accent); }
|
| 181 |
.range-val { font-variant-numeric: tabular-nums; font-weight: 700; color: var(--text); }
|
| 182 |
|
| 183 |
-
.status { font-size: 13px; min-height:
|
|
|
|
| 184 |
.warn { color: var(--danger); font-size: 14px; font-weight: 600; }
|
| 185 |
|
| 186 |
/* ─── Morse chips ─────────────────────────────────────────── */
|
| 187 |
.morse-out {
|
| 188 |
-
min-height:
|
| 189 |
display: flex;
|
| 190 |
flex-wrap: wrap;
|
| 191 |
align-items: center;
|
| 192 |
-
gap:
|
| 193 |
-
padding:
|
| 194 |
background: var(--panel);
|
| 195 |
border: 1px solid var(--line);
|
| 196 |
-
border-radius:
|
| 197 |
}
|
| 198 |
.pattern { display: inline-flex; align-items: center; flex-wrap: wrap; justify-content: center; gap: 4px; max-width: 100%; }
|
| 199 |
.pattern .dot, .pattern .dash {
|
|
@@ -209,28 +208,28 @@ html, body {
|
|
| 209 |
/* ─── Listen ──────────────────────────────────────────────── */
|
| 210 |
.scope {
|
| 211 |
width: 100%;
|
| 212 |
-
height:
|
| 213 |
background: var(--panel);
|
| 214 |
border: 1px solid var(--line);
|
| 215 |
-
border-radius:
|
| 216 |
display: block;
|
| 217 |
}
|
| 218 |
.decoded-text {
|
| 219 |
font-family: "JetBrains Mono", ui-monospace, monospace;
|
| 220 |
-
font-size:
|
| 221 |
font-weight: 700;
|
| 222 |
letter-spacing: 0.04em;
|
| 223 |
-
min-height:
|
| 224 |
word-break: break-word;
|
| 225 |
}
|
| 226 |
-
.decoded-morse { font-family: "JetBrains Mono", ui-monospace, monospace; font-size:
|
| 227 |
-
.transcript { display: flex; flex-direction: column; gap:
|
| 228 |
.line {
|
| 229 |
-
display: flex; flex-direction: column; gap:
|
| 230 |
-
padding:
|
| 231 |
background: var(--panel);
|
| 232 |
border: 1px solid var(--line);
|
| 233 |
-
border-radius:
|
| 234 |
}
|
| 235 |
.line-text { font-family: "JetBrains Mono", monospace; font-weight: 700; font-size: 16px; }
|
| 236 |
.line-morse { font-family: "JetBrains Mono", monospace; font-size: 12px; }
|
|
|
|
| 60 |
display: flex;
|
| 61 |
align-items: center;
|
| 62 |
justify-content: space-between;
|
| 63 |
+
padding: 10px 16px;
|
| 64 |
+
padding-top: max(10px, env(safe-area-inset-top));
|
| 65 |
background: color-mix(in srgb, var(--bg) 88%, transparent);
|
| 66 |
backdrop-filter: blur(12px);
|
| 67 |
border-bottom: 1px solid var(--line);
|
|
|
|
| 82 |
.icon-btn:active { transform: scale(0.95); }
|
| 83 |
|
| 84 |
/* ─── Scrolling single page ───────────────────────────────── */
|
| 85 |
+
.scroller { flex: 1; padding: 12px 14px 40px; display: flex; flex-direction: column; gap: 12px; }
|
| 86 |
+
.divider { width: 100%; border: 0; border-top: 1px solid var(--line); margin: 0; }
|
| 87 |
.view-host { flex: 1; padding: 16px 16px 96px; }
|
| 88 |
+
.view { display: flex; flex-direction: column; gap: 9px; scroll-margin-top: 64px; }
|
| 89 |
+
.view h2 { margin: 0; font-size: 17px; letter-spacing: -0.01em; color: var(--muted); text-transform: uppercase; letter-spacing: 0.06em; }
|
| 90 |
.view h3 { margin: 6px 0; font-size: 15px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.08em; }
|
| 91 |
.muted { color: var(--muted); }
|
| 92 |
|
|
|
|
| 123 |
.tab.active { color: var(--accent); background: color-mix(in srgb, var(--accent) 12%, transparent); }
|
| 124 |
|
| 125 |
/* ─── Fields & controls ───────────────────────────────────── */
|
| 126 |
+
.field { display: flex; flex-direction: column; gap: 6px; }
|
| 127 |
.field-label { font-size: 13px; font-weight: 600; color: var(--muted); }
|
| 128 |
.row { display: flex; gap: 10px; align-items: center; }
|
| 129 |
.row.between { justify-content: space-between; }
|
|
|
|
| 131 |
|
| 132 |
.compose-input {
|
| 133 |
width: 100%;
|
| 134 |
+
padding: 12px 14px;
|
| 135 |
+
font-size: 17px;
|
|
|
|
|
|
|
| 136 |
font-family: inherit;
|
| 137 |
color: var(--text);
|
| 138 |
background: var(--panel);
|
| 139 |
border: 1px solid var(--line);
|
| 140 |
+
border-radius: 12px;
|
| 141 |
outline: none;
|
| 142 |
}
|
| 143 |
.compose-input:focus { border-color: var(--accent); }
|
|
|
|
| 160 |
.btn.big { flex: 1; padding: 16px; font-size: 17px; }
|
| 161 |
.btn.listening { background: linear-gradient(135deg, var(--danger), #ff9a5a); }
|
| 162 |
|
| 163 |
+
.segmented { display: flex; gap: 6px; background: var(--panel); border: 1px solid var(--line); border-radius: 12px; padding: 4px; }
|
| 164 |
.seg {
|
| 165 |
flex: 1;
|
| 166 |
appearance: none;
|
| 167 |
background: transparent;
|
| 168 |
border: 0;
|
| 169 |
color: var(--muted);
|
| 170 |
+
padding: 8px 6px;
|
| 171 |
+
border-radius: 9px;
|
| 172 |
font-size: 14px;
|
| 173 |
font-weight: 600;
|
| 174 |
cursor: pointer;
|
|
|
|
| 178 |
.range { width: 100%; accent-color: var(--accent); }
|
| 179 |
.range-val { font-variant-numeric: tabular-nums; font-weight: 700; color: var(--text); }
|
| 180 |
|
| 181 |
+
.status { font-size: 13px; min-height: 16px; }
|
| 182 |
+
.send-row { margin-top: 0; }
|
| 183 |
.warn { color: var(--danger); font-size: 14px; font-weight: 600; }
|
| 184 |
|
| 185 |
/* ─── Morse chips ─────────────────────────────────────────── */
|
| 186 |
.morse-out {
|
| 187 |
+
min-height: 34px;
|
| 188 |
display: flex;
|
| 189 |
flex-wrap: wrap;
|
| 190 |
align-items: center;
|
| 191 |
+
gap: 5px 10px;
|
| 192 |
+
padding: 10px 12px;
|
| 193 |
background: var(--panel);
|
| 194 |
border: 1px solid var(--line);
|
| 195 |
+
border-radius: 12px;
|
| 196 |
}
|
| 197 |
.pattern { display: inline-flex; align-items: center; flex-wrap: wrap; justify-content: center; gap: 4px; max-width: 100%; }
|
| 198 |
.pattern .dot, .pattern .dash {
|
|
|
|
| 208 |
/* ─── Listen ──────────────────────────────────────────────── */
|
| 209 |
.scope {
|
| 210 |
width: 100%;
|
| 211 |
+
height: 84px;
|
| 212 |
background: var(--panel);
|
| 213 |
border: 1px solid var(--line);
|
| 214 |
+
border-radius: 12px;
|
| 215 |
display: block;
|
| 216 |
}
|
| 217 |
.decoded-text {
|
| 218 |
font-family: "JetBrains Mono", ui-monospace, monospace;
|
| 219 |
+
font-size: 21px;
|
| 220 |
font-weight: 700;
|
| 221 |
letter-spacing: 0.04em;
|
| 222 |
+
min-height: 26px;
|
| 223 |
word-break: break-word;
|
| 224 |
}
|
| 225 |
+
.decoded-morse { font-family: "JetBrains Mono", ui-monospace, monospace; font-size: 13px; word-break: break-all; color: var(--muted); }
|
| 226 |
+
.transcript { display: flex; flex-direction: column; gap: 6px; max-height: 30vh; overflow-y: auto; }
|
| 227 |
.line {
|
| 228 |
+
display: flex; flex-direction: column; gap: 1px;
|
| 229 |
+
padding: 7px 10px;
|
| 230 |
background: var(--panel);
|
| 231 |
border: 1px solid var(--line);
|
| 232 |
+
border-radius: 10px;
|
| 233 |
}
|
| 234 |
.line-text { font-family: "JetBrains Mono", monospace; font-weight: 700; font-size: 16px; }
|
| 235 |
.line-morse { font-family: "JetBrains Mono", monospace; font-size: 12px; }
|
tools/__pycache__/calibrate_audio.cpython-312.pyc
ADDED
|
Binary file (11.6 kB). View file
|
|
|
tools/__pycache__/robot_clap_test.cpython-312.pyc
ADDED
|
Binary file (8.98 kB). View file
|
|
|
views/compose.js
CHANGED
|
@@ -9,9 +9,9 @@ import { tokensToOnsets } from "../lib/wire.js";
|
|
| 9 |
export function createCompose(ctx) {
|
| 10 |
const root = el("section.view#view-compose");
|
| 11 |
|
| 12 |
-
const input = el("
|
|
|
|
| 13 |
placeholder: "Type a message… e.g. SOS",
|
| 14 |
-
rows: 3,
|
| 15 |
value: "SOS",
|
| 16 |
oninput: refresh,
|
| 17 |
});
|
|
@@ -30,17 +30,6 @@ export function createCompose(ctx) {
|
|
| 30 |
(v) => { ctx.state.emitter = v; refresh(); },
|
| 31 |
);
|
| 32 |
|
| 33 |
-
const speed = segmented(
|
| 34 |
-
"Speed",
|
| 35 |
-
[
|
| 36 |
-
{ value: "relaxed", label: "Relaxed" },
|
| 37 |
-
{ value: "normal", label: "Normal" },
|
| 38 |
-
{ value: "brisk", label: "Brisk" },
|
| 39 |
-
],
|
| 40 |
-
ctx.state.speed,
|
| 41 |
-
(v) => { ctx.setSpeed(v); refresh(); },
|
| 42 |
-
);
|
| 43 |
-
|
| 44 |
const sendBtn = el("button.btn.primary.big", { onclick: send }, "Send ▶");
|
| 45 |
const stopBtn = el("button.btn.ghost", { onclick: stop, hidden: true }, "Stop ◼");
|
| 46 |
const status = el("div.status muted");
|
|
@@ -72,8 +61,6 @@ export function createCompose(ctx) {
|
|
| 72 |
}
|
| 73 |
const secs = onsets.length ? (onsets[onsets.length - 1] / 1000 + 0.3) : 0;
|
| 74 |
duration.textContent = onsets.length ? `${onsets.length} taps · ~${secs.toFixed(1)}s` : "";
|
| 75 |
-
// reflect speed selection if changed elsewhere
|
| 76 |
-
speed.setValue(ctx.state.speed);
|
| 77 |
emitter.setValue(ctx.state.emitter);
|
| 78 |
}
|
| 79 |
|
|
@@ -127,13 +114,11 @@ export function createCompose(ctx) {
|
|
| 127 |
|
| 128 |
root.append(
|
| 129 |
el("h2", {}, "Compose"),
|
| 130 |
-
el("label.field-label", {}, "Message"),
|
| 131 |
input,
|
| 132 |
el("div.row.between", {}, [el("span.muted", {}, "Morse preview"), duration]),
|
| 133 |
morseOut,
|
| 134 |
warn,
|
| 135 |
emitter.node,
|
| 136 |
-
speed.node,
|
| 137 |
el("div.row.send-row", {}, [sendBtn, stopBtn]),
|
| 138 |
status,
|
| 139 |
);
|
|
|
|
| 9 |
export function createCompose(ctx) {
|
| 10 |
const root = el("section.view#view-compose");
|
| 11 |
|
| 12 |
+
const input = el("input.compose-input", {
|
| 13 |
+
type: "text",
|
| 14 |
placeholder: "Type a message… e.g. SOS",
|
|
|
|
| 15 |
value: "SOS",
|
| 16 |
oninput: refresh,
|
| 17 |
});
|
|
|
|
| 30 |
(v) => { ctx.state.emitter = v; refresh(); },
|
| 31 |
);
|
| 32 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
const sendBtn = el("button.btn.primary.big", { onclick: send }, "Send ▶");
|
| 34 |
const stopBtn = el("button.btn.ghost", { onclick: stop, hidden: true }, "Stop ◼");
|
| 35 |
const status = el("div.status muted");
|
|
|
|
| 61 |
}
|
| 62 |
const secs = onsets.length ? (onsets[onsets.length - 1] / 1000 + 0.3) : 0;
|
| 63 |
duration.textContent = onsets.length ? `${onsets.length} taps · ~${secs.toFixed(1)}s` : "";
|
|
|
|
|
|
|
| 64 |
emitter.setValue(ctx.state.emitter);
|
| 65 |
}
|
| 66 |
|
|
|
|
| 114 |
|
| 115 |
root.append(
|
| 116 |
el("h2", {}, "Compose"),
|
|
|
|
| 117 |
input,
|
| 118 |
el("div.row.between", {}, [el("span.muted", {}, "Morse preview"), duration]),
|
| 119 |
morseOut,
|
| 120 |
warn,
|
| 121 |
emitter.node,
|
|
|
|
| 122 |
el("div.row.send-row", {}, [sendBtn, stopBtn]),
|
| 123 |
status,
|
| 124 |
);
|
views/dom.js
CHANGED
|
@@ -1,12 +1,19 @@
|
|
| 1 |
/** Tiny DOM helpers (no framework). */
|
| 2 |
|
| 3 |
-
/**
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
export function el(spec, attrs = {}, children = []) {
|
| 5 |
-
const
|
| 6 |
-
const tag =
|
| 7 |
const node = document.createElement(tag);
|
| 8 |
-
|
| 9 |
-
|
|
|
|
|
|
|
|
|
|
| 10 |
for (const [k, v] of Object.entries(attrs)) {
|
| 11 |
if (v == null || v === false) continue;
|
| 12 |
if (k.startsWith("on") && typeof v === "function") {
|
|
|
|
| 1 |
/** Tiny DOM helpers (no framework). */
|
| 2 |
|
| 3 |
+
/**
|
| 4 |
+
* el("button.cls#id", {onclick, ...attrs}, [children|string])
|
| 5 |
+
* Tag is optional (defaults to div); `.class` and `#id` tokens may appear in
|
| 6 |
+
* any order and any count after it (e.g. "section.view#view-compose").
|
| 7 |
+
*/
|
| 8 |
export function el(spec, attrs = {}, children = []) {
|
| 9 |
+
const tagMatch = spec.match(/^[a-z][a-z0-9]*/i);
|
| 10 |
+
const tag = tagMatch ? tagMatch[0] : "div";
|
| 11 |
const node = document.createElement(tag);
|
| 12 |
+
const rest = spec.slice(tagMatch ? tagMatch[0].length : 0);
|
| 13 |
+
const idMatch = rest.match(/#([\w-]+)/);
|
| 14 |
+
if (idMatch) node.id = idMatch[1];
|
| 15 |
+
const classes = [...rest.matchAll(/\.([\w-]+)/g)].map((x) => x[1]);
|
| 16 |
+
if (classes.length) node.className = classes.join(" ");
|
| 17 |
for (const [k, v] of Object.entries(attrs)) {
|
| 18 |
if (v == null || v === false) continue;
|
| 19 |
if (k.startsWith("on") && typeof v === "function") {
|
views/listen.js
CHANGED
|
@@ -117,7 +117,6 @@ export function createListen(ctx) {
|
|
| 117 |
canvas,
|
| 118 |
el("div.row.between", {}, [startBtn, clearBtn]),
|
| 119 |
hint,
|
| 120 |
-
el("label.field-label", {}, "Decoding now"),
|
| 121 |
liveText,
|
| 122 |
liveMorse,
|
| 123 |
el("label.field-label", {}, "Transcript"),
|
|
|
|
| 117 |
canvas,
|
| 118 |
el("div.row.between", {}, [startBtn, clearBtn]),
|
| 119 |
hint,
|
|
|
|
| 120 |
liveText,
|
| 121 |
liveMorse,
|
| 122 |
el("label.field-label", {}, "Transcript"),
|