Newer
Older
bugtrail / packages / extension / src / content / overlay / RecordingComposer.vue
<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>