<script setup lang="ts">
import { onMounted, ref } from "vue";
import browser from "webextension-polyfill";
const emit = defineEmits<{
(e: "saved", reportToken: string): void;
(e: "discarded"): void;
}>();
/** prefill: a starting point — selected so typing replaces it */
const title = ref(`Recording — ${document.title}`);
const description = ref("");
const saving = ref(false);
const error = ref<string | null>(null);
const titleInput = ref<HTMLInputElement>();
/** two-step discard: first click arms the button, second drops the recording */
const confirmingDiscard = ref(false);
onMounted(() => {
titleInput.value?.focus();
titleInput.value?.select();
});
async function save() {
if (saving.value) return;
saving.value = true;
error.value = null;
try {
const response = (await browser.runtime.sendMessage({
type: "recorder_submit",
title: title.value,
description: description.value,
})) as { ok: boolean; data?: { report_token: string | null }; error?: string };
if (!response?.ok) throw new Error(response?.error ?? "Submit failed");
emit("saved", response.data?.report_token ?? "");
} catch (err) {
error.value = err instanceof Error ? err.message : String(err);
} finally {
saving.value = false;
}
}
async function discard() {
if (!confirmingDiscard.value) {
confirmingDiscard.value = true;
return;
}
const response = (await browser.runtime.sendMessage({ type: "recorder_discard" })) as {
ok: boolean;
error?: string;
};
if (response?.ok) emit("discarded");
}
function onKeyDown(event: KeyboardEvent) {
// Escape deliberately does nothing: an accidental Esc must not drop the
// recording — discarding is an explicit two-step button
if (event.key === "Enter" && (event.ctrlKey || event.metaKey) && !saving.value) {
event.preventDefault();
event.stopPropagation();
void save();
}
}
</script>
<template>
<div class="reccomposer-backdrop" @keydown="onKeyDown">
<div class="reccomposer">
<div class="reccomposer-header">
<span class="reccomposer-title">Recording finished</span>
<span class="reccomposer-hint">Ctrl+Enter saves</span>
</div>
<label class="reccomposer-field">
<span>Title</span>
<input ref="titleInput" v-model="title" type="text" class="reccomposer-input" />
</label>
<label class="reccomposer-field">
<span>Description</span>
<textarea v-model="description" rows="4" class="reccomposer-input" placeholder="What happened? Repro notes, expected behavior…" />
</label>
<div v-if="error" class="reccomposer-error">{{ error }}</div>
<div class="reccomposer-actions">
<button type="button" class="reccomposer-btn reccomposer-btn-danger" :class="{ 'is-armed': confirmingDiscard }" @click="discard">
{{ confirmingDiscard ? "Discard recording?" : "Discard" }}
</button>
<button type="button" class="reccomposer-btn reccomposer-btn-primary" :disabled="saving" @click="save">
{{ saving ? "Saving…" : "Save report" }}
</button>
</div>
</div>
</div>
</template>
<style scoped>
.reccomposer-backdrop {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background: rgba(10, 12, 20, 0.6);
}
.reccomposer {
display: flex;
flex-direction: column;
gap: 10px;
width: min(640px, calc(100vw - 32px));
max-height: calc(100vh - 48px);
overflow-y: auto;
padding: 16px;
background: var(--color-surface, #1a1c2b);
border: 2px solid var(--color-accent, #7aa2f7);
color: var(--color-text, #c0caf5);
}
.reccomposer-header {
display: flex;
align-items: center;
justify-content: space-between;
}
.reccomposer-title {
font-size: 13px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
}
.reccomposer-hint {
font-size: 11px;
opacity: 0.55;
}
.reccomposer-field {
display: flex;
flex-direction: column;
gap: 4px;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.reccomposer-input {
padding: 6px 8px;
border: 2px solid var(--color-border, #2f334d);
background: var(--color-bg, #10121c);
color: var(--color-text, #c0caf5);
font: inherit;
font-size: 13px;
text-transform: none;
letter-spacing: normal;
}
.reccomposer-input:focus {
outline: none;
border-color: var(--color-accent, #7aa2f7);
}
.reccomposer-error {
padding: 6px 8px;
border: 2px solid var(--color-danger, #f7768e);
color: var(--color-danger, #f7768e);
font-size: 12px;
}
.reccomposer-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
}
.reccomposer-btn {
padding: 6px 14px;
border: 2px solid var(--color-border, #2f334d);
font: inherit;
font-size: 12px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
cursor: pointer;
}
.reccomposer-btn-primary {
background: var(--color-accent, #7aa2f7);
border-color: var(--color-accent, #7aa2f7);
color: var(--color-bg, #10121c);
}
.reccomposer-btn-primary:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.reccomposer-btn-danger {
background: transparent;
color: inherit;
}
.reccomposer-btn-danger:hover,
.reccomposer-btn-danger.is-armed {
border-color: var(--color-danger, #f7768e);
color: var(--color-danger, #f7768e);
}
</style>