Skip to content

Commit afe1ec9

Browse files
committed
feat(web): make the transcript editor discoverable
The edit gestures (double-click a word, drag a boundary's ⋮) worked but nothing on the page said so -- a word looked identical to plain text and the split/boundary controls were invisible until an accidental hover landed on them. Add a standing hint line, rest-state affordances on the word/split/boundary controls, an error color distinct from normal status text, a primary/secondary split between Transcribe and the two exports, short transitions on state changes, and the light half of the already-declared color-scheme.
1 parent dfcf322 commit afe1ec9

2 files changed

Lines changed: 102 additions & 8 deletions

File tree

‎site/main.js‎

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ modelSelect.value = DEFAULT_MODEL
105105
const modelLabel = el('label', 'field-label', 'Model')
106106
modelLabel.htmlFor = modelSelect.id
107107

108-
const transcribeButton = el('button', 'action', 'Transcribe')
108+
const transcribeButton = el('button', 'action is-primary', 'Transcribe')
109109
transcribeButton.type = 'button'
110110
transcribeButton.disabled = true
111111

@@ -129,6 +129,13 @@ progress.hidden = true
129129
const controls = el('div', 'controls')
130130
controls.append(modelLabel, modelSelect, transcribeButton, srtButton, vttButton)
131131

132+
// Empty until a transcript exists (`.transcript-hint:empty` hides it), so the
133+
// two gestures nothing else on the page states -- double-click a word,
134+
// drag ⋮ -- are said once rather than left for hover to reveal by accident.
135+
const transcriptHint = el('p', 'transcript-hint')
136+
const TRANSCRIPT_HINT_TEXT =
137+
'Double-click a word to fix it · drag ⋮ between segments to move where one ends.'
138+
132139
const transcriptEl = el('div', 'transcript')
133140

134141
const footer = el(
@@ -148,6 +155,7 @@ app.append(
148155
surface,
149156
audio,
150157
controls,
158+
transcriptHint,
151159
transcriptEl,
152160
progress,
153161
engineStatus,
@@ -201,6 +209,7 @@ function updateExportState() {
201209
engineStatus.textContent = transcript
202210
? `${words} words in ${transcript.segments.length} segments.`
203211
: 'No transcript yet.'
212+
transcriptHint.textContent = words > 0 ? TRANSCRIPT_HINT_TEXT : ''
204213
}
205214

206215
// Ctrl/Cmd+Z to undo, Ctrl/Cmd+Shift+Z or Ctrl/Cmd+Y to redo. Attached to
@@ -237,6 +246,7 @@ async function open(file) {
237246
if (!file) return
238247

239248
status.textContent = `Decoding ${file.name}…`
249+
status.classList.remove('is-error')
240250
try {
241251
const decoded = await decodeAudioFile(file)
242252

@@ -266,6 +276,7 @@ async function open(file) {
266276
transcribeButton.disabled = false
267277
} catch (error) {
268278
status.textContent = message(error)
279+
status.classList.add('is-error')
269280
}
270281
}
271282

@@ -304,6 +315,7 @@ async function transcribe() {
304315
modelSelect.disabled = true
305316
progress.hidden = false
306317
progress.removeAttribute('value')
318+
engineStatus.classList.remove('is-error')
307319

308320
try {
309321
if (loadedModel !== id) {
@@ -358,6 +370,7 @@ async function transcribe() {
358370
transcriptView.render(transcript)
359371
} catch (error) {
360372
engineStatus.textContent = message(error)
373+
engineStatus.classList.add('is-error')
361374
} finally {
362375
progress.hidden = true
363376
transcribeButton.disabled = false

‎site/styles.css‎

Lines changed: 88 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
--muted: #9c968c;
1515
--accent: #d8a657;
1616
--line: #2b2825;
17+
--error: #e0685a;
1718

1819
--sp-1: 0.5rem;
1920
--sp-2: 1rem;
@@ -24,6 +25,20 @@
2425
--mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
2526
}
2627

28+
/* The light half of `color-scheme: light dark` above. Same token names, so
29+
every rule that consumes them -- including src/web/waveform.js, which
30+
reads colors back via getComputedStyle -- repaints with no other change. */
31+
@media (prefers-color-scheme: light) {
32+
:root {
33+
--bg: #faf8f4;
34+
--fg: #201d18;
35+
--muted: #756e62;
36+
--accent: #9a6a1f;
37+
--line: #ded7c9;
38+
--error: #b3392b;
39+
}
40+
}
41+
2742
* {
2843
box-sizing: border-box;
2944
}
@@ -71,6 +86,7 @@ noscript p {
7186
border-radius: 4px;
7287
color: var(--accent);
7388
cursor: pointer;
89+
transition: background-color 140ms ease-out;
7490
}
7591

7692
.chooser:hover,
@@ -102,6 +118,12 @@ noscript p {
102118
color: var(--muted);
103119
}
104120

121+
/* A decode or transcription failure, as opposed to ordinary progress text --
122+
the two were previously indistinguishable, both rendering in --muted. */
123+
.status.is-error {
124+
color: var(--error);
125+
}
126+
105127
/* The waveform and its playhead.
106128
107129
The canvas is drawn once per file and once per resize; the playhead is this
@@ -153,6 +175,20 @@ noscript p {
153175
width: 100%;
154176
}
155177

178+
/* States the two gestures a transcript supports that no element on its own
179+
makes visible: double-click to correct a word, drag the ⋮ to move a
180+
boundary. Shown once, next to the transcript, rather than relying on
181+
someone finding either by accidental hover. */
182+
.transcript-hint {
183+
margin: 0 0 var(--sp-1);
184+
font-size: 0.8125rem;
185+
color: var(--muted);
186+
}
187+
188+
.transcript-hint:empty {
189+
display: none;
190+
}
191+
156192
/* The transcript: one <p> per segment, one clickable <span> per word.
157193
Scrollable rather than growing the page without bound -- a long recording
158194
produces a transcript much taller than the waveform above it. */
@@ -181,6 +217,11 @@ noscript p {
181217
.transcript-word {
182218
cursor: pointer;
183219
border-radius: 3px;
220+
/* Present at rest, not only on hover -- a dotted underline is the
221+
difference between "clickable" and "plain text," which nothing else
222+
about a <span> communicates. */
223+
border-bottom: 1px dotted color-mix(in srgb, var(--muted) 60%, transparent);
224+
transition: background-color 140ms ease-out;
184225
}
185226

186227
.transcript-word:hover {
@@ -208,27 +249,47 @@ noscript p {
208249
font: inherit;
209250
}
210251

211-
/* The split control: a sliver between two words that only becomes a visible
212-
seam on hover or focus, so the transcript does not read as perforated at
213-
every word boundary while idle. */
252+
/* The split control: a sliver between two words. Its full-width fill only
253+
appears on hover or focus, so the transcript does not read as perforated
254+
at every word boundary while idle -- but a faint rest-state mark (below)
255+
still makes it findable by scanning rather than only by accidental hover. */
214256
.transcript-split {
215257
display: inline-block;
216-
width: 0.5rem;
258+
position: relative;
259+
width: 0.75rem;
217260
height: 1em;
218261
vertical-align: middle;
219-
margin: 0 -0.25rem;
262+
margin: 0 -0.375rem;
220263
padding: 0;
221264
border: 0;
222265
border-radius: 2px;
223266
background: transparent;
224267
cursor: col-resize;
268+
transition: background-color 140ms ease-out;
269+
}
270+
271+
/* A faint mark at rest -- findable by scanning the transcript, not only by
272+
an accidental hover -- that brightens into the full hover/focus fill. */
273+
.transcript-split::after {
274+
content: '';
275+
position: absolute;
276+
top: 15%;
277+
bottom: 15%;
278+
left: 50%;
279+
width: 1px;
280+
background: color-mix(in srgb, var(--muted) 45%, transparent);
225281
}
226282

227283
.transcript-split:hover,
228284
.transcript-split:focus-visible {
229285
background: color-mix(in srgb, var(--accent) 35%, transparent);
230286
}
231287

288+
.transcript-split:hover::after,
289+
.transcript-split:focus-visible::after {
290+
background: transparent;
291+
}
292+
232293
/* Between segments: the drag handle and the merge control share this row,
233294
the way controls sharing a boundary in a timeline editor usually do -- one
234295
strip of affordances rather than two competing for the same pixels. */
@@ -237,13 +298,13 @@ noscript p {
237298
align-items: center;
238299
justify-content: center;
239300
gap: var(--sp-1);
240-
height: 1.5rem;
301+
height: 1.75rem;
241302
color: var(--muted);
242303
}
243304

244305
.transcript-boundary-handle,
245306
.transcript-merge {
246-
padding: 0 var(--sp-1);
307+
padding: var(--sp-1) var(--sp-1);
247308
border: 1px dashed var(--line);
248309
border-radius: 3px;
249310
background: transparent;
@@ -252,10 +313,15 @@ noscript p {
252313
font-size: 0.75rem;
253314
line-height: 1.4;
254315
cursor: pointer;
316+
transition: border-color 140ms ease-out, color 140ms ease-out;
255317
}
256318

257319
.transcript-boundary-handle {
258320
cursor: row-resize;
321+
/* Bigger than its glyph alone -- ⋮ in a one-line row is a hard target to
322+
land a drag on; the padding above widens it without changing the row's
323+
visual weight. */
324+
min-width: 2rem;
259325
}
260326

261327
.transcript-boundary-handle::before {
@@ -302,6 +368,7 @@ noscript p {
302368
color: var(--fg);
303369
font: inherit;
304370
font-size: 0.9375rem;
371+
transition: background-color 140ms ease-out, border-color 140ms ease-out, color 140ms ease-out;
305372
}
306373

307374
.action {
@@ -313,6 +380,20 @@ noscript p {
313380
color: var(--accent);
314381
}
315382

383+
/* Transcribe is the one control that moves the page from "nothing happened
384+
yet" to "there's a transcript" -- everything else (the two downloads) only
385+
makes sense after it, so it carries the accent fill they don't. */
386+
.action.is-primary {
387+
border-color: var(--accent);
388+
background: var(--accent);
389+
color: var(--bg);
390+
}
391+
392+
.action.is-primary:hover:not(:disabled) {
393+
background: color-mix(in srgb, var(--accent) 85%, var(--fg));
394+
color: var(--bg);
395+
}
396+
316397
/* Disabled controls are dimmed rather than hidden. Every one of them is
317398
disabled for a reason the status line states -- no file open, no transcript
318399
yet -- and a control that vanishes leaves that sentence answering a question

0 commit comments

Comments
 (0)