Rudder's corner is a pill that shows it is hearing you, and its draft scrolls like a message thread
Collapsed, the pill reads "Rudder · voice to text". While recording it drops the label for a red dot, a mic that lights only while speech is detected, and the last few words on one truncated line, so a closed panel still shows the reader is being heard. Rudder reports this through an `activity` event. Open, the panel no longer scrolls: the draft fills it and scrolls on its own, stays editable while recording, and notices sit above it; with settings or info open the panel scrolls so they stay reachable.
This commit is contained in:
parent
533ef04a9f
commit
4e94e92160
13 changed files with 72 additions and 28 deletions
|
|
@ -22,7 +22,7 @@
|
||||||
"cap2": "إعداد مسبق",
|
"cap2": "إعداد مسبق",
|
||||||
"cap1": "مفرد",
|
"cap1": "مفرد",
|
||||||
"stageExperimental": "تجريبي",
|
"stageExperimental": "تجريبي",
|
||||||
"widget:rudder-voice/rudder-voice": "Rudder — تحويل الصوت إلى نص",
|
"widget:rudder-voice/rudder-voice": "Rudder · تحويل الصوت إلى نص",
|
||||||
"toggleList": "إظهار القائمة أو إخفاؤها",
|
"toggleList": "إظهار القائمة أو إخفاؤها",
|
||||||
"openViews": "فتح قائمة العروض",
|
"openViews": "فتح قائمة العروض",
|
||||||
"expandList": "توسيع القائمة",
|
"expandList": "توسيع القائمة",
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@
|
||||||
"cap2": "Voreinstellung",
|
"cap2": "Voreinstellung",
|
||||||
"cap1": "Einzeln",
|
"cap1": "Einzeln",
|
||||||
"stageExperimental": "Experimentell",
|
"stageExperimental": "Experimentell",
|
||||||
"widget:rudder-voice/rudder-voice": "Rudder — Sprache zu Text",
|
"widget:rudder-voice/rudder-voice": "Rudder · Sprache zu Text",
|
||||||
"toggleList": "Liste ein-/ausblenden",
|
"toggleList": "Liste ein-/ausblenden",
|
||||||
"openViews": "Ansichtsliste öffnen",
|
"openViews": "Ansichtsliste öffnen",
|
||||||
"expandList": "Liste ausklappen",
|
"expandList": "Liste ausklappen",
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@
|
||||||
"cap2": "Preset",
|
"cap2": "Preset",
|
||||||
"cap1": "Single",
|
"cap1": "Single",
|
||||||
"stageExperimental": "Experimental",
|
"stageExperimental": "Experimental",
|
||||||
"widget:rudder-voice/rudder-voice": "Rudder — voice to text",
|
"widget:rudder-voice/rudder-voice": "Rudder · voice to text",
|
||||||
"toggleList": "Toggle list",
|
"toggleList": "Toggle list",
|
||||||
"openViews": "Open the list of views",
|
"openViews": "Open the list of views",
|
||||||
"expandList": "Expand list",
|
"expandList": "Expand list",
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@
|
||||||
"cap2": "Preajuste",
|
"cap2": "Preajuste",
|
||||||
"cap1": "Individual",
|
"cap1": "Individual",
|
||||||
"stageExperimental": "Experimental",
|
"stageExperimental": "Experimental",
|
||||||
"widget:rudder-voice/rudder-voice": "Rudder — voz a texto",
|
"widget:rudder-voice/rudder-voice": "Rudder · voz a texto",
|
||||||
"toggleList": "Mostrar u ocultar la lista",
|
"toggleList": "Mostrar u ocultar la lista",
|
||||||
"openViews": "Abrir la lista de vistas",
|
"openViews": "Abrir la lista de vistas",
|
||||||
"expandList": "Expandir la lista",
|
"expandList": "Expandir la lista",
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@
|
||||||
"cap2": "Préréglage",
|
"cap2": "Préréglage",
|
||||||
"cap1": "Simple",
|
"cap1": "Simple",
|
||||||
"stageExperimental": "Expérimental",
|
"stageExperimental": "Expérimental",
|
||||||
"widget:rudder-voice/rudder-voice": "Rudder — voix vers texte",
|
"widget:rudder-voice/rudder-voice": "Rudder · voix vers texte",
|
||||||
"toggleList": "Afficher ou masquer la liste",
|
"toggleList": "Afficher ou masquer la liste",
|
||||||
"openViews": "Ouvrir la liste des vues",
|
"openViews": "Ouvrir la liste des vues",
|
||||||
"expandList": "Déplier la liste",
|
"expandList": "Déplier la liste",
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@
|
||||||
"cap2": "प्रीसेट",
|
"cap2": "प्रीसेट",
|
||||||
"cap1": "एकल",
|
"cap1": "एकल",
|
||||||
"stageExperimental": "प्रायोगिक",
|
"stageExperimental": "प्रायोगिक",
|
||||||
"widget:rudder-voice/rudder-voice": "Rudder — आवाज़ से टेक्स्ट",
|
"widget:rudder-voice/rudder-voice": "Rudder · आवाज़ से टेक्स्ट",
|
||||||
"toggleList": "सूची दिखाएँ या छिपाएँ",
|
"toggleList": "सूची दिखाएँ या छिपाएँ",
|
||||||
"openViews": "दृश्यों की सूची खोलें",
|
"openViews": "दृश्यों की सूची खोलें",
|
||||||
"expandList": "सूची फैलाएँ",
|
"expandList": "सूची फैलाएँ",
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@
|
||||||
"cap2": "Preimpostato",
|
"cap2": "Preimpostato",
|
||||||
"cap1": "Singolo",
|
"cap1": "Singolo",
|
||||||
"stageExperimental": "Sperimentale",
|
"stageExperimental": "Sperimentale",
|
||||||
"widget:rudder-voice/rudder-voice": "Rudder — voce in testo",
|
"widget:rudder-voice/rudder-voice": "Rudder · voce in testo",
|
||||||
"toggleList": "Mostra o nascondi l’elenco",
|
"toggleList": "Mostra o nascondi l’elenco",
|
||||||
"openViews": "Apri l’elenco delle viste",
|
"openViews": "Apri l’elenco delle viste",
|
||||||
"expandList": "Espandi l’elenco",
|
"expandList": "Espandi l’elenco",
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@
|
||||||
"cap2": "Predefinição",
|
"cap2": "Predefinição",
|
||||||
"cap1": "Único",
|
"cap1": "Único",
|
||||||
"stageExperimental": "Experimental",
|
"stageExperimental": "Experimental",
|
||||||
"widget:rudder-voice/rudder-voice": "Rudder — voz para texto",
|
"widget:rudder-voice/rudder-voice": "Rudder · voz para texto",
|
||||||
"toggleList": "Mostrar ou ocultar a lista",
|
"toggleList": "Mostrar ou ocultar a lista",
|
||||||
"openViews": "Abrir a lista de visualizações",
|
"openViews": "Abrir a lista de visualizações",
|
||||||
"expandList": "Expandir a lista",
|
"expandList": "Expandir a lista",
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@
|
||||||
"cap2": "Presetat",
|
"cap2": "Presetat",
|
||||||
"cap1": "Simplu",
|
"cap1": "Simplu",
|
||||||
"stageExperimental": "Experimental",
|
"stageExperimental": "Experimental",
|
||||||
"widget:rudder-voice/rudder-voice": "Rudder — voce în text",
|
"widget:rudder-voice/rudder-voice": "Rudder · voce în text",
|
||||||
"toggleList": "Afișează sau ascunde lista",
|
"toggleList": "Afișează sau ascunde lista",
|
||||||
"openViews": "Deschide lista de vizualizări",
|
"openViews": "Deschide lista de vizualizări",
|
||||||
"expandList": "Extinde lista",
|
"expandList": "Extinde lista",
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@
|
||||||
"cap2": "Предустановка",
|
"cap2": "Предустановка",
|
||||||
"cap1": "Одиночный",
|
"cap1": "Одиночный",
|
||||||
"stageExperimental": "Экспериментальные",
|
"stageExperimental": "Экспериментальные",
|
||||||
"widget:rudder-voice/rudder-voice": "Rudder — голос в текст",
|
"widget:rudder-voice/rudder-voice": "Rudder · голос в текст",
|
||||||
"toggleList": "Показать или скрыть список",
|
"toggleList": "Показать или скрыть список",
|
||||||
"openViews": "Открыть список представлений",
|
"openViews": "Открыть список представлений",
|
||||||
"expandList": "Развернуть список",
|
"expandList": "Развернуть список",
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@
|
||||||
"cap2": "预设",
|
"cap2": "预设",
|
||||||
"cap1": "单一",
|
"cap1": "单一",
|
||||||
"stageExperimental": "实验性",
|
"stageExperimental": "实验性",
|
||||||
"widget:rudder-voice/rudder-voice": "Rudder — 语音转文字",
|
"widget:rudder-voice/rudder-voice": "Rudder · 语音转文字",
|
||||||
"toggleList": "显示或隐藏列表",
|
"toggleList": "显示或隐藏列表",
|
||||||
"openViews": "打开视图列表",
|
"openViews": "打开视图列表",
|
||||||
"expandList": "展开列表",
|
"expandList": "展开列表",
|
||||||
|
|
|
||||||
|
|
@ -471,21 +471,32 @@ const WIDGET_CSS = `
|
||||||
[data-studio-widgets^="top"] [data-widget]{flex-direction:column-reverse}
|
[data-studio-widgets^="top"] [data-widget]{flex-direction:column-reverse}
|
||||||
[data-widget-panel]{display:none;position:relative;width:360px;max-width:calc(100vw - 32px);height:min(480px,calc(100vh - 110px));height:min(480px,calc(100dvh - 110px - env(safe-area-inset-top) - env(safe-area-inset-bottom)));border-radius:16px;overflow:hidden;background:#0b0f14;box-shadow:0 12px 40px rgba(0,0,0,.35)}
|
[data-widget-panel]{display:none;position:relative;width:360px;max-width:calc(100vw - 32px);height:min(480px,calc(100vh - 110px));height:min(480px,calc(100dvh - 110px - env(safe-area-inset-top) - env(safe-area-inset-bottom)));border-radius:16px;overflow:hidden;background:#0b0f14;box-shadow:0 12px 40px rgba(0,0,0,.35)}
|
||||||
[data-widget-panel][data-open]{display:block}
|
[data-widget-panel][data-open]{display:block}
|
||||||
[data-widget-bubble]{position:relative;width:56px;height:56px;padding:0;border:0;border-radius:50%;background:#0284c7;color:#fff;cursor:pointer;display:grid;place-items:center;box-shadow:0 4px 16px rgba(0,0,0,.3)}
|
[data-widget-bubble]{position:relative;display:inline-flex;align-items:center;gap:8px;height:48px;max-width:min(360px,calc(100vw - 32px));padding:0 18px 0 14px;border:1px solid #334155;border-radius:999px;background:#0f172a;color:#e5e7eb;font:600 14px/1 system-ui,-apple-system,"Segoe UI",sans-serif;cursor:pointer;box-shadow:0 4px 16px rgba(0,0,0,.3)}
|
||||||
[data-widget-bubble]:focus-visible{outline:3px solid #7dd3fc;outline-offset:3px}
|
[data-widget-bubble]:focus-visible{outline:3px solid #7dd3fc;outline-offset:3px}
|
||||||
[data-widget-bubble] svg{width:26px;height:26px;display:block}
|
[data-wb-icon]{display:grid;place-items:center;flex:none;color:#38bdf8}
|
||||||
[data-widget-bubble][data-listening="true"]{background:#dc2626;animation:studio-widget-pulse 1.4s ease-out infinite}
|
[data-wb-icon] svg,[data-wb-chev] svg{width:20px;height:20px;display:block}
|
||||||
[data-widget-bubble][data-listening="true"]::after{content:"";position:absolute;top:3px;right:3px;width:12px;height:12px;border-radius:50%;background:#fff;border:2px solid #dc2626}
|
[data-wb-text]{min-width:0;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}
|
||||||
@keyframes studio-widget-pulse{0%{box-shadow:0 0 0 0 rgba(220,38,38,.6)}70%{box-shadow:0 0 0 14px rgba(220,38,38,0)}100%{box-shadow:0 0 0 0 rgba(220,38,38,0)}}
|
[data-wb-dot],[data-wb-chev]{display:none;flex:none}
|
||||||
@media (prefers-reduced-motion:reduce){[data-widget-bubble][data-listening="true"]{animation:none}}
|
[data-wb-dot]{width:10px;height:10px;border-radius:50%;background:#ef4444}
|
||||||
|
[data-widget-bubble][aria-expanded="true"] [data-wb-chev]{display:block}
|
||||||
|
[data-widget-bubble][data-listening="true"]{border-color:#ef4444;background:#1f0f12;animation:studio-widget-pulse 1.4s ease-out infinite}
|
||||||
|
[data-widget-bubble][data-listening="true"] [data-wb-dot]{display:block}
|
||||||
|
[data-widget-bubble][data-listening="true"] [data-wb-icon]{color:#6b7280}
|
||||||
|
[data-widget-bubble][data-speaking="true"] [data-wb-icon]{color:#4ade80;animation:studio-widget-speak .5s ease-in-out infinite alternate}
|
||||||
|
@keyframes studio-widget-pulse{0%{box-shadow:0 0 0 0 rgba(239,68,68,.55)}70%{box-shadow:0 0 0 12px rgba(239,68,68,0)}100%{box-shadow:0 0 0 0 rgba(239,68,68,0)}}
|
||||||
|
@keyframes studio-widget-speak{from{transform:scale(1)}to{transform:scale(1.18)}}
|
||||||
|
@media (prefers-reduced-motion:reduce){[data-widget-bubble][data-listening="true"],[data-widget-bubble][data-speaking="true"] [data-wb-icon]{animation:none}}
|
||||||
@media (max-width:480px){
|
@media (max-width:480px){
|
||||||
[data-studio-widgets^="bottom"]{bottom:max(8px,env(safe-area-inset-bottom))}
|
[data-studio-widgets^="bottom"]{bottom:max(8px,env(safe-area-inset-bottom))}
|
||||||
[data-studio-widgets$="right"]{right:max(8px,env(safe-area-inset-right))}
|
[data-studio-widgets$="right"]{right:max(8px,env(safe-area-inset-right))}
|
||||||
[data-studio-widgets$="left"]{left:max(8px,env(safe-area-inset-left))}
|
[data-studio-widgets$="left"]{left:max(8px,env(safe-area-inset-left))}
|
||||||
[data-widget-panel]{width:calc(100vw - 16px);max-width:none;height:60vh;height:60dvh}
|
[data-widget-panel]{width:calc(100vw - 16px);max-width:none;height:60vh;height:60dvh}
|
||||||
|
[data-widget-bubble]{max-width:calc(100vw - 16px)}
|
||||||
}`
|
}`
|
||||||
const WIDGET_ICON_OPEN = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="2" width="6" height="12" rx="3"/><path d="M5 10v1a7 7 0 0 0 14 0v-1"/><path d="M12 18v4"/></svg>'
|
const WIDGET_ICON_OPEN = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="2" width="6" height="12" rx="3"/><path d="M5 10v1a7 7 0 0 0 14 0v-1"/><path d="M12 18v4"/></svg>'
|
||||||
const WIDGET_ICON_CLOSE = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" aria-hidden="true"><path d="M6 9l6 6 6-6"/></svg>'
|
const WIDGET_ICON_CLOSE = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" aria-hidden="true"><path d="M6 9l6 6 6-6"/></svg>'
|
||||||
|
// The pill: [recording dot] [mic] [label, or the last words while recording] [chevron when open]
|
||||||
|
const WIDGET_PILL = '<span data-wb-dot aria-hidden="true"></span><span data-wb-icon>' + WIDGET_ICON_OPEN + '</span><span data-wb-text></span><span data-wb-chev>' + WIDGET_ICON_CLOSE + '</span>'
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'PluginStudio',
|
name: 'PluginStudio',
|
||||||
|
|
@ -1131,7 +1142,8 @@ export default defineComponent({
|
||||||
btn.setAttribute('aria-label', wl !== wk ? wl : row.label)
|
btn.setAttribute('aria-label', wl !== wk ? wl : row.label)
|
||||||
btn.title = btn.getAttribute('aria-label')
|
btn.title = btn.getAttribute('aria-label')
|
||||||
btn.setAttribute('aria-expanded', 'false')
|
btn.setAttribute('aria-expanded', 'false')
|
||||||
btn.innerHTML = WIDGET_ICON_OPEN
|
btn.innerHTML = WIDGET_PILL
|
||||||
|
btn.querySelector('[data-wb-text]').textContent = btn.getAttribute('aria-label')
|
||||||
btn.addEventListener('click', () => this.openWidget(row, !panel.hasAttribute('data-open')))
|
btn.addEventListener('click', () => this.openWidget(row, !panel.hasAttribute('data-open')))
|
||||||
box.appendChild(panel); box.appendChild(btn); host.appendChild(box)
|
box.appendChild(panel); box.appendChild(btn); host.appendChild(box)
|
||||||
}
|
}
|
||||||
|
|
@ -1151,7 +1163,6 @@ export default defineComponent({
|
||||||
// transcript stays where it was.
|
// transcript stays where it was.
|
||||||
if (open) panel.setAttribute('data-open', ''); else panel.removeAttribute('data-open')
|
if (open) panel.setAttribute('data-open', ''); else panel.removeAttribute('data-open')
|
||||||
btn.setAttribute('aria-expanded', open ? 'true' : 'false')
|
btn.setAttribute('aria-expanded', open ? 'true' : 'false')
|
||||||
btn.innerHTML = open ? WIDGET_ICON_CLOSE : WIDGET_ICON_OPEN
|
|
||||||
try { localStorage.setItem('plugin-studio:widget-open:' + row.key, open ? '1' : '0') } catch (_) {}
|
try { localStorage.setItem('plugin-studio:widget-open:' + row.key, open ? '1' : '0') } catch (_) {}
|
||||||
const W = this.widgetStore()
|
const W = this.widgetStore()
|
||||||
if (!open || W.els[row.key]) return
|
if (!open || W.els[row.key]) return
|
||||||
|
|
@ -1165,10 +1176,21 @@ export default defineComponent({
|
||||||
el.style.cssText = 'display:block;position:absolute;inset:0;width:100%;height:100%;overflow-y:auto;overscroll-behavior:contain;'
|
el.style.cssText = 'display:block;position:absolute;inset:0;width:100%;height:100%;overflow-y:auto;overscroll-behavior:contain;'
|
||||||
// LIVE MIC, VISIBLE WHEN COLLAPSED: a widget that emits `recording` (Rudder does) turns its bubble
|
// LIVE MIC, VISIBLE WHEN COLLAPSED: a widget that emits `recording` (Rudder does) turns its bubble
|
||||||
// red, pulsing, with a dot while it listens, so closing the panel never hides that the mic is on.
|
// red, pulsing, with a dot while it listens, so closing the panel never hides that the mic is on.
|
||||||
|
// While recording the pill drops its label for the last words heard, and its mic lights only while
|
||||||
|
// speech is detected (`activity`), so a collapsed Rudder still shows it is hearing the reader.
|
||||||
|
const pill = box.querySelector('[data-widget-bubble]')
|
||||||
|
const text = pill.querySelector('[data-wb-text]')
|
||||||
el.addEventListener('recording', (e) => {
|
el.addEventListener('recording', (e) => {
|
||||||
const on = !!(Array.isArray(e.detail) ? e.detail[0] : e.detail)
|
const on = !!(Array.isArray(e.detail) ? e.detail[0] : e.detail)
|
||||||
const b = box.querySelector('[data-widget-bubble]')
|
pill.setAttribute('data-listening', on ? 'true' : 'false')
|
||||||
if (b) b.setAttribute('data-listening', on ? 'true' : 'false')
|
if (!on) { pill.setAttribute('data-speaking', 'false'); text.textContent = pill.getAttribute('aria-label') }
|
||||||
|
else text.textContent = ''
|
||||||
|
})
|
||||||
|
el.addEventListener('activity', (e) => {
|
||||||
|
const a = (Array.isArray(e.detail) ? e.detail[0] : e.detail) || {}
|
||||||
|
if (pill.getAttribute('data-listening') !== 'true') return
|
||||||
|
pill.setAttribute('data-speaking', a.speaking ? 'true' : 'false')
|
||||||
|
text.textContent = a.words || ''
|
||||||
})
|
})
|
||||||
W.els[row.key] = el
|
W.els[row.key] = el
|
||||||
panel.appendChild(el)
|
panel.appendChild(el)
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ const props = defineProps({ profileConfig: { type: String, default: '' }, varian
|
||||||
const compact = computed(() => props.variant === 'widget')
|
const compact = computed(() => props.variant === 'widget')
|
||||||
// `recording` (true/false) tells a host that Rudder started or stopped listening — the Plugin Studio's
|
// `recording` (true/false) tells a host that Rudder started or stopped listening — the Plugin Studio's
|
||||||
// corner button turns red with it, so a collapsed corner still shows the mic is live.
|
// corner button turns red with it, so a collapsed corner still shows the mic is live.
|
||||||
const emit = defineEmits(['recording'])
|
const emit = defineEmits(['recording', 'activity'])
|
||||||
|
|
||||||
const cfg = computed(() => {
|
const cfg = computed(() => {
|
||||||
try { return props.profileConfig ? JSON.parse(props.profileConfig) : {} } catch { return {} }
|
try { return props.profileConfig ? JSON.parse(props.profileConfig) : {} } catch { return {} }
|
||||||
|
|
@ -545,6 +545,23 @@ onBeforeUnmount(() => {
|
||||||
|
|
||||||
watch(recording, (v) => emit('recording', v))
|
watch(recording, (v) => emit('recording', v))
|
||||||
|
|
||||||
|
// `activity` feeds a host's collapsed pill while recording: whether speech is heard right now (the level,
|
||||||
|
// held briefly so the icon doesn't flicker between words) and the last few words, so a reader with the
|
||||||
|
// panel closed can see they are being heard.
|
||||||
|
let speakingUntil = 0, activityTimer = null, lastActivity = ''
|
||||||
|
function sendActivity() {
|
||||||
|
activityTimer = null
|
||||||
|
const speaking = recording.value && Date.now() < speakingUntil
|
||||||
|
const tail = recording.value ? draft.value.trim().split(/\s+/).slice(-5).join(' ') : ''
|
||||||
|
const key = speaking + '|' + tail
|
||||||
|
if (key === lastActivity) return
|
||||||
|
lastActivity = key
|
||||||
|
emit('activity', { speaking, words: tail })
|
||||||
|
}
|
||||||
|
function queueActivity() { if (!activityTimer) activityTimer = setTimeout(sendActivity, 150) }
|
||||||
|
watch(level, (v) => { if (v > 0.12) speakingUntil = Date.now() + 400; queueActivity(); setTimeout(queueActivity, 450) })
|
||||||
|
watch([draft, recording], queueActivity)
|
||||||
|
|
||||||
const statusText = computed(() => ({
|
const statusText = computed(() => ({
|
||||||
connecting: 'Connecting…',
|
connecting: 'Connecting…',
|
||||||
starting: 'Loading speech model… (about 20 s the first time)',
|
starting: 'Loading speech model… (about 20 s the first time)',
|
||||||
|
|
@ -556,7 +573,7 @@ const statusText = computed(() => ({
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="rudder" :class="{ rec: recording, compact }">
|
<div class="rudder" :class="{ rec: recording, compact, more: compact && (showSettings || showInfo) }">
|
||||||
<header v-if="compact" class="bar">
|
<header v-if="compact" class="bar">
|
||||||
<b class="name">Rudder</b>
|
<b class="name">Rudder</b>
|
||||||
<div class="status" :data-state="engine" role="status">
|
<div class="status" :data-state="engine" role="status">
|
||||||
|
|
@ -779,7 +796,10 @@ code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 1
|
||||||
.debug dt { color: var(--muted); }
|
.debug dt { color: var(--muted); }
|
||||||
.debug dd { margin: 0; overflow-wrap: anywhere; }
|
.debug dd { margin: 0; overflow-wrap: anywhere; }
|
||||||
/* The corner panel (variant="widget"): no page header, the draft fills the panel. */
|
/* The corner panel (variant="widget"): no page header, the draft fills the panel. */
|
||||||
.rudder.compact { min-height: 100%; padding: 10px 12px 12px; gap: 8px; font-size: 14px; }
|
.rudder.compact { height: 100%; min-height: 0; overflow: hidden; padding: 10px 12px 12px; gap: 8px; font-size: 14px; }
|
||||||
|
/* Like a messaging app: the panel itself never scrolls; the draft does. With settings or info open, the
|
||||||
|
panel scrolls instead so they stay reachable. */
|
||||||
|
.rudder.compact.more { overflow-y: auto; }
|
||||||
.bar { display: flex; align-items: center; gap: 8px; }
|
.bar { display: flex; align-items: center; gap: 8px; }
|
||||||
.bar .name { font-size: 14px; }
|
.bar .name { font-size: 14px; }
|
||||||
.bar .status { padding: 3px 9px; font-size: 12px; gap: 6px; }
|
.bar .status { padding: 3px 9px; font-size: 12px; gap: 6px; }
|
||||||
|
|
@ -788,15 +808,17 @@ code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 1
|
||||||
.icon-btn svg { width: 17px; height: 17px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; }
|
.icon-btn svg { width: 17px; height: 17px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; }
|
||||||
.icon-btn[aria-expanded='true'], .icon-btn:hover { color: var(--accent); border-color: var(--accent); }
|
.icon-btn[aria-expanded='true'], .icon-btn:hover { color: var(--accent); border-color: var(--accent); }
|
||||||
.compact .controls .mic { display: none; }
|
.compact .controls .mic { display: none; }
|
||||||
.compact .compose { min-height: 140px; }
|
.compact .compose { flex: 1 1 0; min-height: 120px; }
|
||||||
.compact .draft { min-height: 120px; resize: none; padding: 10px 12px; font-size: 15px; line-height: 1.5; }
|
.compact.more .compose { flex: 0 0 auto; height: 200px; }
|
||||||
|
.compact .draft { flex: 1 1 0; min-height: 0; overflow-y: auto; overscroll-behavior: contain; -webkit-overflow-scrolling: touch;
|
||||||
|
resize: none; padding: 10px 12px; font-size: 16px; line-height: 1.5; }
|
||||||
.compact .hearing { display: none; }
|
.compact .hearing { display: none; }
|
||||||
.compact .bottom { order: 2; gap: 8px; }
|
.compact .bottom { order: 2; gap: 8px; }
|
||||||
.compact .bottom button { padding: 7px 12px; }
|
.compact .bottom button { padding: 7px 12px; }
|
||||||
.compact .bottom .mic.sm { padding: 7px 14px; font-size: 14px; gap: 6px; border-radius: 999px; }
|
.compact .bottom .mic.sm { padding: 9px 18px; font-size: 15px; gap: 6px; border-radius: 999px; }
|
||||||
.compact .bottom .mic.sm:not(.on) { background: var(--accent); color: #04121c; border-color: var(--accent); }
|
.compact .bottom .mic.sm:not(.on) { background: var(--accent); color: #04121c; border-color: var(--accent); }
|
||||||
.compact .bottom .mic.sm.on { background: var(--rec); color: #fff; border-color: var(--rec); }
|
.compact .bottom .mic.sm.on { background: var(--rec); color: #fff; border-color: var(--rec); }
|
||||||
.compact .notice, .compact .hint { order: 3; font-size: 12.5px; padding: 8px 10px; }
|
.compact .notice, .compact .hint { order: 0; font-size: 12.5px; padding: 8px 10px; }
|
||||||
.compact .debug { order: 4; }
|
.compact .debug { order: 4; }
|
||||||
/* A phone-sized panel: less padding, and a shorter draft so it stays in view below the controls. */
|
/* A phone-sized panel: less padding, and a shorter draft so it stays in view below the controls. */
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue