feat(ui): live notes pane, click-to-annotate transcript, resizable split
Granola-style redesign: the live-recording view gains a Notes pane (bound to recording.notesText) alongside the transcript, and each transcript line is clickable to attach/edit a note for that moment (recording.segmentNotes, badge on annotated lines). Also, for both the live and finalized-meeting layouts: the transcript/notes split is now drag-resizable and each side independently collapsible (Splitter + layout store), and the post-finalize Notes pane defaults to Preview (rendered) instead of raw Editor mode, since the merged document is now properly sectioned rather than a single-speaker blob.
This commit is contained in:
1 parent
0027becd10
commit
99d50583cf
1 file changed
+403
-115
@@ -8,6 +8,8 @@
|
||||
import { api, type SpeakerInfo } from "../api";
|
||||
import { renderMarkdown } from "../markdown";
|
||||
import { save, open } from "@tauri-apps/plugin-dialog";
|
||||
import { layout, clamp } from "../stores/layout.svelte";
|
||||
import Splitter from "../components/Splitter.svelte";
|
||||
import {
|
||||
Bold,
|
||||
Italic,
|
||||
@@ -23,6 +25,10 @@
|
||||
NotebookPen,
|
||||
Eye,
|
||||
Pencil,
|
||||
PanelLeftClose,
|
||||
PanelLeftOpen,
|
||||
PanelRightClose,
|
||||
PanelRightOpen,
|
||||
} from "@lucide/svelte";
|
||||
|
||||
function speakerName(label: string, speakers: SpeakerInfo[] = []): string {
|
||||
@@ -40,11 +46,50 @@
|
||||
let notesText = $state("");
|
||||
// Notes is a single pane: raw markdown ("Editor") or the rendered result
|
||||
// ("Preview"), toggled by one button whose label flips to the other mode.
|
||||
let notesPreview = $state(false);
|
||||
// Defaults to Preview: the merged notes.md is now a properly sectioned
|
||||
// document (## Notes / ## Transcript), not worth reading as raw markdown
|
||||
// by default the way the old single-speaker-blob output arguably was.
|
||||
let notesPreview = $state(true);
|
||||
let editorEl: HTMLTextAreaElement | undefined = $state();
|
||||
let saveTimer: ReturnType<typeof setTimeout> | undefined;
|
||||
let loadedForId: string | null = null;
|
||||
|
||||
// Transcript/notes split (FR-UX-1): resizable (drag the Splitter) and
|
||||
// each side independently hideable, shared across the finalized-meeting
|
||||
// and live-recording views via the layout store. `splitWidth` tracks the
|
||||
// container's current pixel width (bound below) so a drag delta in
|
||||
// pixels can be converted to a fraction of the available space.
|
||||
let splitWidth = $state(600);
|
||||
function splitColumns(): string {
|
||||
if (layout.transcriptCollapsed) return "auto auto 1fr";
|
||||
if (layout.notesCollapsed) return "1fr auto auto";
|
||||
return `${layout.transcriptFraction * 100}% auto ${(1 - layout.transcriptFraction) * 100}%`;
|
||||
}
|
||||
function onSplitResize(deltaPx: number) {
|
||||
if (splitWidth <= 0) return;
|
||||
layout.transcriptFraction = clamp(layout.transcriptFraction + deltaPx / splitWidth, 0.2, 0.8);
|
||||
}
|
||||
function toggleTranscript() {
|
||||
const wouldCollapse = !layout.transcriptCollapsed;
|
||||
if (wouldCollapse && layout.notesCollapsed) return; // never hide both
|
||||
layout.transcriptCollapsed = wouldCollapse;
|
||||
layout.persist();
|
||||
}
|
||||
function toggleNotes() {
|
||||
const wouldCollapse = !layout.notesCollapsed;
|
||||
if (wouldCollapse && layout.transcriptCollapsed) return;
|
||||
layout.notesCollapsed = wouldCollapse;
|
||||
layout.persist();
|
||||
}
|
||||
|
||||
// Live-recording transcript: which segment (by start_ms, the note anchor —
|
||||
// see recording.svelte.ts) is showing its note-entry field, if any.
|
||||
let selectedSegmentMs = $state<number | null>(null);
|
||||
$effect(() => {
|
||||
void recording.meetingId; // dependency: reset the open note field for a new recording
|
||||
selectedSegmentMs = null;
|
||||
});
|
||||
|
||||
// Sync the editor buffer whenever a different meeting is selected.
|
||||
$effect(() => {
|
||||
const m = meetings.selected;
|
||||
@@ -163,131 +208,287 @@
|
||||
onchange={onTitleChange}
|
||||
aria-label="Meeting title"
|
||||
/>
|
||||
<div class="split">
|
||||
<div class="pane transcript">
|
||||
<h4>
|
||||
<MessageSquareText size={14} aria-hidden="true" /> Transcript
|
||||
<span class="badge lang" title="Transcription language">{languageLabel(m.language)}</span
|
||||
<div
|
||||
class="split"
|
||||
style="grid-template-columns: {splitColumns()};"
|
||||
bind:clientWidth={splitWidth}
|
||||
>
|
||||
<div class="pane transcript" class:collapsed={layout.transcriptCollapsed}>
|
||||
{#if layout.transcriptCollapsed}
|
||||
<button
|
||||
class="pane-toggle"
|
||||
onclick={toggleTranscript}
|
||||
title="Show transcript"
|
||||
aria-label="Show transcript"
|
||||
>
|
||||
</h4>
|
||||
{#if m.recorded && settings.models.some((mo) => mo.installed)}
|
||||
{@const reprocessModelInfo = settings.models.find((mo) => mo.id === reprocessModel)}
|
||||
<div class="reprocess">
|
||||
<select bind:value={reprocessModel}>
|
||||
<option value="">Re-transcribe with…</option>
|
||||
{#each settings.models.filter((mo) => mo.installed) as mo (mo.id)}
|
||||
<option value={mo.id}>{mo.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{#if reprocessModelInfo?.multilingual}
|
||||
<select bind:value={reprocessLanguage} aria-label="Reprocess language">
|
||||
<option value="">Keep current language</option>
|
||||
<option value="auto">Auto-detect</option>
|
||||
{#each settings.languages as l (l.code)}
|
||||
<option value={l.code}>{l.label}</option>
|
||||
<PanelLeftOpen size={16} aria-hidden="true" />
|
||||
</button>
|
||||
{:else}
|
||||
<h4>
|
||||
<MessageSquareText size={14} aria-hidden="true" /> Transcript
|
||||
<span class="badge lang" title="Transcription language"
|
||||
>{languageLabel(m.language)}</span
|
||||
>
|
||||
<button
|
||||
class="pane-toggle inline"
|
||||
onclick={toggleTranscript}
|
||||
title="Hide transcript"
|
||||
aria-label="Hide transcript"
|
||||
>
|
||||
<PanelLeftClose size={13} aria-hidden="true" />
|
||||
</button>
|
||||
</h4>
|
||||
{#if m.recorded && settings.models.some((mo) => mo.installed)}
|
||||
{@const reprocessModelInfo = settings.models.find((mo) => mo.id === reprocessModel)}
|
||||
<div class="reprocess">
|
||||
<select bind:value={reprocessModel}>
|
||||
<option value="">Re-transcribe with…</option>
|
||||
{#each settings.models.filter((mo) => mo.installed) as mo (mo.id)}
|
||||
<option value={mo.id}>{mo.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{/if}
|
||||
<button disabled={!reprocessModel || reprocessing} onclick={reprocess}>
|
||||
<RefreshCw size={13} aria-hidden="true" class={reprocessing ? "spin" : ""} />
|
||||
{reprocessing ? "Re-transcribing…" : "Go"}
|
||||
{#if reprocessModelInfo?.multilingual}
|
||||
<select bind:value={reprocessLanguage} aria-label="Reprocess language">
|
||||
<option value="">Keep current language</option>
|
||||
<option value="auto">Auto-detect</option>
|
||||
{#each settings.languages as l (l.code)}
|
||||
<option value={l.code}>{l.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{/if}
|
||||
<button disabled={!reprocessModel || reprocessing} onclick={reprocess}>
|
||||
<RefreshCw size={13} aria-hidden="true" class={reprocessing ? "spin" : ""} />
|
||||
{reprocessing ? "Re-transcribing…" : "Go"}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
{#if m.segments.length === 0}
|
||||
<p class="muted">No transcript for this meeting.</p>
|
||||
{:else}
|
||||
{#each m.segments as s (s.id)}
|
||||
<p><strong>{speakerName(s.speaker, m.speakers)}:</strong> {s.text}</p>
|
||||
{/each}
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{#if !layout.transcriptCollapsed && !layout.notesCollapsed}
|
||||
<Splitter
|
||||
label="Resize transcript and notes"
|
||||
onResize={onSplitResize}
|
||||
onResizeEnd={() => layout.persist()}
|
||||
/>
|
||||
{:else}
|
||||
<span></span>
|
||||
{/if}
|
||||
<div class="pane notes" class:collapsed={layout.notesCollapsed}>
|
||||
{#if layout.notesCollapsed}
|
||||
<button
|
||||
class="pane-toggle"
|
||||
onclick={toggleNotes}
|
||||
title="Show notes"
|
||||
aria-label="Show notes"
|
||||
>
|
||||
<PanelRightOpen size={16} aria-hidden="true" />
|
||||
</button>
|
||||
{:else}
|
||||
<h4>
|
||||
<NotebookPen size={14} aria-hidden="true" /> Notes
|
||||
<button
|
||||
class="pane-toggle inline"
|
||||
onclick={toggleNotes}
|
||||
title="Hide notes"
|
||||
aria-label="Hide notes"
|
||||
>
|
||||
<PanelRightClose size={13} aria-hidden="true" />
|
||||
</button>
|
||||
</h4>
|
||||
<div class="toolbar" role="toolbar" aria-label="Notes formatting">
|
||||
<button onclick={() => wrapSelection("**")} title="Bold" aria-label="Bold">
|
||||
<Bold size={14} aria-hidden="true" />
|
||||
</button>
|
||||
<button onclick={() => wrapSelection("_")} title="Italic" aria-label="Italic">
|
||||
<Italic size={14} aria-hidden="true" />
|
||||
</button>
|
||||
<button onclick={() => insertLinePrefix("# ")} title="Heading 1" aria-label="Heading 1">
|
||||
<Heading1 size={14} aria-hidden="true" />
|
||||
</button>
|
||||
<button
|
||||
onclick={() => insertLinePrefix("## ")}
|
||||
title="Heading 2"
|
||||
aria-label="Heading 2"
|
||||
>
|
||||
<Heading2 size={14} aria-hidden="true" />
|
||||
</button>
|
||||
<button
|
||||
onclick={() => insertLinePrefix("- ")}
|
||||
title="Bullet list"
|
||||
aria-label="Bullet list"
|
||||
>
|
||||
<List size={14} aria-hidden="true" />
|
||||
</button>
|
||||
<button
|
||||
onclick={() => insertLinePrefix("- [ ] ")}
|
||||
title="Checkbox"
|
||||
aria-label="Checkbox list item"
|
||||
>
|
||||
<ListChecks size={14} aria-hidden="true" />
|
||||
</button>
|
||||
<button
|
||||
class="toggle"
|
||||
onclick={() => (notesPreview = !notesPreview)}
|
||||
title={notesPreview ? "Edit the raw markdown" : "Render the markdown"}
|
||||
aria-pressed={notesPreview}
|
||||
>
|
||||
{#if notesPreview}
|
||||
<Pencil size={14} aria-hidden="true" />
|
||||
Editor
|
||||
{:else}
|
||||
<Eye size={14} aria-hidden="true" />
|
||||
Preview
|
||||
{/if}
|
||||
</button>
|
||||
<span class="spacer"></span>
|
||||
<button onclick={exportMd} title="Export notes as .md">
|
||||
<FileText size={13} aria-hidden="true" />
|
||||
.md
|
||||
</button>
|
||||
<button onclick={exportPdf} title="Export notes as .pdf">
|
||||
<FileDown size={13} aria-hidden="true" />
|
||||
PDF
|
||||
</button>
|
||||
<button onclick={exportDocx} title="Export notes as .docx">
|
||||
<FileDown size={13} aria-hidden="true" />
|
||||
Word
|
||||
</button>
|
||||
<button onclick={exportBundle} title="Export audio + transcript + notes to a folder">
|
||||
<FolderOutput size={13} aria-hidden="true" />
|
||||
Bundle
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
{#if m.segments.length === 0}
|
||||
<p class="muted">No transcript for this meeting.</p>
|
||||
{:else}
|
||||
{#each m.segments as s (s.id)}
|
||||
<p><strong>{speakerName(s.speaker, m.speakers)}:</strong> {s.text}</p>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
<div class="pane notes">
|
||||
<h4><NotebookPen size={14} aria-hidden="true" /> Notes</h4>
|
||||
<div class="toolbar" role="toolbar" aria-label="Notes formatting">
|
||||
<button onclick={() => wrapSelection("**")} title="Bold" aria-label="Bold">
|
||||
<Bold size={14} aria-hidden="true" />
|
||||
</button>
|
||||
<button onclick={() => wrapSelection("_")} title="Italic" aria-label="Italic">
|
||||
<Italic size={14} aria-hidden="true" />
|
||||
</button>
|
||||
<button onclick={() => insertLinePrefix("# ")} title="Heading 1" aria-label="Heading 1">
|
||||
<Heading1 size={14} aria-hidden="true" />
|
||||
</button>
|
||||
<button onclick={() => insertLinePrefix("## ")} title="Heading 2" aria-label="Heading 2">
|
||||
<Heading2 size={14} aria-hidden="true" />
|
||||
</button>
|
||||
<button
|
||||
onclick={() => insertLinePrefix("- ")}
|
||||
title="Bullet list"
|
||||
aria-label="Bullet list"
|
||||
>
|
||||
<List size={14} aria-hidden="true" />
|
||||
</button>
|
||||
<button
|
||||
onclick={() => insertLinePrefix("- [ ] ")}
|
||||
title="Checkbox"
|
||||
aria-label="Checkbox list item"
|
||||
>
|
||||
<ListChecks size={14} aria-hidden="true" />
|
||||
</button>
|
||||
<button
|
||||
class="toggle"
|
||||
onclick={() => (notesPreview = !notesPreview)}
|
||||
title={notesPreview ? "Edit the raw markdown" : "Render the markdown"}
|
||||
aria-pressed={notesPreview}
|
||||
>
|
||||
<div class="editor-preview">
|
||||
{#if notesPreview}
|
||||
<Pencil size={14} aria-hidden="true" />
|
||||
Editor
|
||||
<!-- eslint-disable-next-line svelte/no-at-html-tags -- sanitized via renderMarkdown() -->
|
||||
<div class="preview">{@html renderMarkdown(notesText)}</div>
|
||||
{:else}
|
||||
<Eye size={14} aria-hidden="true" />
|
||||
Preview
|
||||
<textarea
|
||||
bind:this={editorEl}
|
||||
bind:value={notesText}
|
||||
oninput={scheduleSave}
|
||||
placeholder="Notes…"
|
||||
></textarea>
|
||||
{/if}
|
||||
</button>
|
||||
<span class="spacer"></span>
|
||||
<button onclick={exportMd} title="Export notes as .md">
|
||||
<FileText size={13} aria-hidden="true" />
|
||||
.md
|
||||
</button>
|
||||
<button onclick={exportPdf} title="Export notes as .pdf">
|
||||
<FileDown size={13} aria-hidden="true" />
|
||||
PDF
|
||||
</button>
|
||||
<button onclick={exportDocx} title="Export notes as .docx">
|
||||
<FileDown size={13} aria-hidden="true" />
|
||||
Word
|
||||
</button>
|
||||
<button onclick={exportBundle} title="Export audio + transcript + notes to a folder">
|
||||
<FolderOutput size={13} aria-hidden="true" />
|
||||
Bundle
|
||||
</button>
|
||||
</div>
|
||||
<div class="editor-preview">
|
||||
{#if notesPreview}
|
||||
<!-- eslint-disable-next-line svelte/no-at-html-tags -- sanitized via renderMarkdown() -->
|
||||
<div class="preview">{@html renderMarkdown(notesText)}</div>
|
||||
{:else}
|
||||
<textarea
|
||||
bind:this={editorEl}
|
||||
bind:value={notesText}
|
||||
oninput={scheduleSave}
|
||||
placeholder="Notes…"
|
||||
></textarea>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{:else if recording.segments.length === 0}
|
||||
{:else if recording.state === "idle"}
|
||||
<p class="muted pad">Transcript will appear here as you record.</p>
|
||||
{:else}
|
||||
<div class="pad">
|
||||
{#each recording.segments as s (s.id)}
|
||||
<p class:interim={s.interim}>
|
||||
<strong>{speakerName(s.speaker)}:</strong>
|
||||
{s.text}
|
||||
</p>
|
||||
{/each}
|
||||
<!-- Granola-style redesign: the Notes pane is open and typable while
|
||||
recording, and clicking a transcript line attaches a note to that
|
||||
moment — both merged into notes.md with the transcript at stop. -->
|
||||
<div
|
||||
class="split"
|
||||
style="grid-template-columns: {splitColumns()};"
|
||||
bind:clientWidth={splitWidth}
|
||||
>
|
||||
<div class="pane transcript" class:collapsed={layout.transcriptCollapsed}>
|
||||
{#if layout.transcriptCollapsed}
|
||||
<button
|
||||
class="pane-toggle"
|
||||
onclick={toggleTranscript}
|
||||
title="Show transcript"
|
||||
aria-label="Show transcript"
|
||||
>
|
||||
<PanelLeftOpen size={16} aria-hidden="true" />
|
||||
</button>
|
||||
{:else}
|
||||
<h4>
|
||||
<MessageSquareText size={14} aria-hidden="true" /> Transcript
|
||||
<button
|
||||
class="pane-toggle inline"
|
||||
onclick={toggleTranscript}
|
||||
title="Hide transcript"
|
||||
aria-label="Hide transcript"
|
||||
>
|
||||
<PanelLeftClose size={13} aria-hidden="true" />
|
||||
</button>
|
||||
</h4>
|
||||
{#if recording.segments.length === 0}
|
||||
<p class="muted">Transcript will appear here as you record.</p>
|
||||
{:else}
|
||||
{#each recording.segments as s (s.id)}
|
||||
{@const hasNote = recording.segmentNotes.has(s.start_ms)}
|
||||
{@const open = selectedSegmentMs === s.start_ms}
|
||||
<div class="segment">
|
||||
<button
|
||||
type="button"
|
||||
class="segment-line"
|
||||
class:interim={s.interim}
|
||||
class:active={open}
|
||||
onclick={() => (selectedSegmentMs = open ? null : s.start_ms)}
|
||||
>
|
||||
<strong>{speakerName(s.speaker)}:</strong>
|
||||
{s.text}
|
||||
{#if hasNote}
|
||||
<span class="note-badge" title="Has a note">📝</span>
|
||||
{/if}
|
||||
</button>
|
||||
{#if open}
|
||||
<input
|
||||
type="text"
|
||||
class="segment-note-input"
|
||||
placeholder="Add a note for this moment…"
|
||||
value={recording.segmentNotes.get(s.start_ms) ?? ""}
|
||||
onchange={(e) =>
|
||||
recording.setSegmentNote(s.start_ms, (e.target as HTMLInputElement).value)}
|
||||
aria-label="Note for this transcript line"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{#if !layout.transcriptCollapsed && !layout.notesCollapsed}
|
||||
<Splitter
|
||||
label="Resize transcript and notes"
|
||||
onResize={onSplitResize}
|
||||
onResizeEnd={() => layout.persist()}
|
||||
/>
|
||||
{:else}
|
||||
<span></span>
|
||||
{/if}
|
||||
<div class="pane notes" class:collapsed={layout.notesCollapsed}>
|
||||
{#if layout.notesCollapsed}
|
||||
<button
|
||||
class="pane-toggle"
|
||||
onclick={toggleNotes}
|
||||
title="Show notes"
|
||||
aria-label="Show notes"
|
||||
>
|
||||
<PanelRightOpen size={16} aria-hidden="true" />
|
||||
</button>
|
||||
{:else}
|
||||
<h4>
|
||||
<NotebookPen size={14} aria-hidden="true" /> Notes
|
||||
<button
|
||||
class="pane-toggle inline"
|
||||
onclick={toggleNotes}
|
||||
title="Hide notes"
|
||||
aria-label="Hide notes"
|
||||
>
|
||||
<PanelRightClose size={13} aria-hidden="true" />
|
||||
</button>
|
||||
</h4>
|
||||
<textarea
|
||||
class="live-notes"
|
||||
value={recording.notesText}
|
||||
oninput={(e) => recording.setNotesText((e.target as HTMLTextAreaElement).value)}
|
||||
placeholder="Type notes while you talk…"
|
||||
></textarea>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -328,9 +529,71 @@
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.segment {
|
||||
margin: 0.2rem 0;
|
||||
}
|
||||
.segment-line {
|
||||
display: block;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-left: 2px solid transparent;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
line-height: 1.5;
|
||||
padding: 0.15rem 0.4rem;
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
transition: background-color 120ms ease-out;
|
||||
}
|
||||
.segment-line:hover {
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
.segment-line.active {
|
||||
background: var(--accent-soft);
|
||||
border-left-color: var(--accent);
|
||||
}
|
||||
.segment-line.interim {
|
||||
opacity: 0.55;
|
||||
font-style: italic;
|
||||
}
|
||||
.note-badge {
|
||||
margin-left: 0.3rem;
|
||||
}
|
||||
.segment-note-input {
|
||||
display: block;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
margin: 0.2rem 0 0.4rem;
|
||||
padding: 0.3rem 0.5rem;
|
||||
border: 1px solid var(--accent);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--bg);
|
||||
color: var(--fg);
|
||||
font: inherit;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.live-notes {
|
||||
resize: none;
|
||||
width: 100%;
|
||||
height: calc(100% - 2rem);
|
||||
box-sizing: border-box;
|
||||
padding: 0.6rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--bg);
|
||||
color: var(--fg);
|
||||
font: inherit;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.live-notes:focus-visible {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.split {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
/* grid-template-columns set inline — depends on resize/collapse state (FR-UX-1). */
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
@@ -342,6 +605,12 @@
|
||||
.pane.transcript {
|
||||
border-right: 1px solid var(--border);
|
||||
}
|
||||
.pane.collapsed {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
padding: 0.4rem;
|
||||
}
|
||||
.pane h4 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -353,6 +622,25 @@
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--muted);
|
||||
}
|
||||
.pane-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: none;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--muted);
|
||||
cursor: pointer;
|
||||
padding: 0.3rem;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
.pane-toggle:hover {
|
||||
background: var(--bg-hover);
|
||||
color: var(--fg);
|
||||
}
|
||||
.pane-toggle.inline {
|
||||
margin-left: auto;
|
||||
}
|
||||
/* Transcription language (T8.7, FR-TRX-4) — a quiet pill, not a status
|
||||
color, since "which language" isn't a good/bad state to flag. */
|
||||
.badge.lang {
|
||||
|
||||
Reference in new issue
Block a user