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 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 r4 antenna-fix + one-page";
 
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: 14px 18px calc(14px + env(safe-area-inset-top));
64
- padding-top: max(14px, 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,11 +82,11 @@ html, body {
82
  .icon-btn:active { transform: scale(0.95); }
83
 
84
  /* ─── Scrolling single page ───────────────────────────────── */
85
- .scroller { flex: 1; padding: 16px 16px 48px; display: flex; flex-direction: column; gap: 22px; }
86
- .divider { width: 100%; border: 0; border-top: 1px solid var(--line); margin: 2px 0; }
87
  .view-host { flex: 1; padding: 16px 16px 96px; }
88
- .view { display: flex; flex-direction: column; gap: 16px; scroll-margin-top: 72px; }
89
- .view h2 { margin: 4px 0 0; font-size: 24px; letter-spacing: -0.02em; }
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: 8px; }
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
- resize: vertical;
135
- min-height: 84px;
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: var(--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: 14px; padding: 5px; }
166
  .seg {
167
  flex: 1;
168
  appearance: none;
169
  background: transparent;
170
  border: 0;
171
  color: var(--muted);
172
- padding: 10px 8px;
173
- border-radius: 10px;
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: 18px; }
 
184
  .warn { color: var(--danger); font-size: 14px; font-weight: 600; }
185
 
186
  /* ─── Morse chips ─────────────────────────────────────────── */
187
  .morse-out {
188
- min-height: 48px;
189
  display: flex;
190
  flex-wrap: wrap;
191
  align-items: center;
192
- gap: 6px 12px;
193
- padding: 14px;
194
  background: var(--panel);
195
  border: 1px solid var(--line);
196
- border-radius: var(--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: 130px;
213
  background: var(--panel);
214
  border: 1px solid var(--line);
215
- border-radius: var(--radius);
216
  display: block;
217
  }
218
  .decoded-text {
219
  font-family: "JetBrains Mono", ui-monospace, monospace;
220
- font-size: 26px;
221
  font-weight: 700;
222
  letter-spacing: 0.04em;
223
- min-height: 34px;
224
  word-break: break-word;
225
  }
226
- .decoded-morse { font-family: "JetBrains Mono", ui-monospace, monospace; font-size: 14px; word-break: break-all; }
227
- .transcript { display: flex; flex-direction: column; gap: 8px; }
228
  .line {
229
- display: flex; flex-direction: column; gap: 2px;
230
- padding: 10px 12px;
231
  background: var(--panel);
232
  border: 1px solid var(--line);
233
- border-radius: 12px;
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("textarea.compose-input", {
 
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
- /** el("button.cls#id", {onclick, ...attrs}, [children|string]) */
 
 
 
 
4
  export function el(spec, attrs = {}, children = []) {
5
- const m = spec.match(/^([a-z0-9]+)?(#[\w-]+)?((?:\.[\w-]+)*)$/i) || [];
6
- const tag = m[1] || "div";
7
  const node = document.createElement(tag);
8
- if (m[2]) node.id = m[2].slice(1);
9
- if (m[3]) node.className = m[3].split(".").filter(Boolean).join(" ");
 
 
 
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"),