<script setup lang="ts">
import { computed, nextTick, onMounted, ref, watch } from "vue";
import { useNotesStore } from "../stores/notes";
import ColorPicker from "./ColorPicker.vue";
import Icon from "./Icon.vue";
import LabelPicker from "./LabelPicker.vue";
import LinkPreview from "./LinkPreview.vue";
import { fromLocalInput, toLocalInput } from "../notes/datetime";
import { takeMorphOrigin } from "../composables/useEditorMorph";
import { prefersReducedMotion } from "../composables/useReducedMotion";
import type { Note, NoteLabel, NoteRevision } from "../stores/notes";
import { LABEL_CHIP_CLASSES, resolveLabelColor, type NoteColor } from "../notes/colors";
import {
  afterEnter,
  type EditorBlock,
  joinBlocks,
  plusTask,
  splitBlocks,
  withoutIndex,
} from "../notes/blocks";

// One modal editor for BOTH composing and editing — a single surface (the board's
// "Take a note…" bar just opens this in compose mode). `note` = the note being edited,
// or null to compose a new one. In compose mode there is no note id until a draft is
// persisted — on commit-with-content, or on the first rich action (label / image /
// reminder / checklist) — so we never litter empty notes.
const props = withDefaults(defineProps<{ note?: Note | null; initialBody?: string }>(), {
  note: null,
  initialBody: "",
});
const emit = defineEmits<{ (e: "close"): void; (e: "navigate", id: string): void }>();
const notes = useNotesStore();

const noteId = ref<string | null>(props.note?.id ?? null);
// BLOCKS rather than one string, because a checklist item is drawn as a real checkbox
// and a widget cannot live inside a <textarea>. The note is still one markdown body —
// see notes/blocks.ts — and `body` is what every save, baseline and draft still reads.
const blocks = ref<EditorBlock[]>(splitBlocks(props.note?.body ?? props.initialBody));
const body = computed(() => joinBlocks(blocks.value));
function setBody(text: string): void {
  blocks.value = splitBlocks(text);
}
const color = ref<NoteColor>(props.note?.color ?? "default");
const labelList = ref<NoteLabel[]>(props.note ? [...props.note.labels] : []);
// Whether this editor is showing the checklist. A note HAS a checklist (M13 step 2)
// rather than BEING one, so this is a view flag, not a property of the note: it turns
// on when the note already carries items, and when someone asks for one.
const saving = ref(false);
const root = ref<HTMLElement | null>(null);
// One element per block, keyed by the block's id — the only thing about a block that
// survives one being inserted above it. Focus is asked for by id and honoured after
// the render that created the field, since an element that does not exist yet cannot
// take it.
const blockEls = new Map<number, HTMLTextAreaElement | HTMLInputElement>();
function setBlockEl(id: number, el: unknown): void {
  if (el) blockEls.set(id, el as HTMLTextAreaElement);
  else blockEls.delete(id);
}
async function focusBlock(id: number | null): Promise<void> {
  if (id === null) return;
  await nextTick();
  const el = blockEls.get(id);
  el?.focus();
  if (el) el.selectionStart = el.selectionEnd = el.value.length;
}

/** A prose field sized to its text. `rows="1"` plus this beats guessing a row count,
 *  which is wrong the moment a line wraps. */
function grow(el: HTMLTextAreaElement): void {
  el.style.height = "auto";
  el.style.height = `${el.scrollHeight}px`;
}
function growAll(): void {
  for (const el of blockEls.values()) {
    if (el instanceof HTMLTextAreaElement) grow(el);
  }
}
const fileInput = ref<HTMLInputElement | null>(null);
const uploadError = ref("");

// Baseline for edit-mode change detection (save only when text actually changed).
const baseline = ref<{ body: string; color: NoteColor }>({
  body: props.note?.body ?? "",
  color: (props.note?.color ?? "default") as NoteColor,
});

const isCreate = computed(() => noteId.value === null);
const hasContent = computed(() => body.value.trim() !== "");
// Rich features need a saved note; in compose they light up once there's content.
const richEnabled = computed(() => !isCreate.value || hasContent.value);

// A synthetic note for compose mode (before anything is persisted), so the shared
// template can read attachments/items/remind_at uniformly.
const draftNote = computed<Note>(() => ({
  id: "",
  display_title: "",
  body: body.value,
  color: color.value,
  position: 0,
  pinned: false,
  archived: false,
  trashed: false,
  deleted_at: null,
  remind_at: null,
  recurrence: null,
  labels: labelList.value,
  items: [],
  attachments: [],
  previews: [],
  created_at: null,
  updated_at: null,
}));
const liveNote = computed<Note>(() =>
  noteId.value
    ? (notes.items.find((n) => n.id === noteId.value) ?? props.note ?? draftNote.value)
    : draftNote.value,
);
const bodyPlaceholder = "Take a note…";

// Keep local state in sync when the edited note changes (modal reused for another note).
watch(
  () => props.note,
  (n) => {
    noteId.value = n?.id ?? null;
    setBody(n?.body ?? "");
    color.value = (n?.color ?? "default") as NoteColor;
    labelList.value = n ? [...n.labels] : [];
    baseline.value = { body: n?.body ?? "", color: (n?.color ?? "default") as NoteColor };
  },
);

// ---- persistence ----
async function createFromFields(): Promise<void> {
  const created = await notes.create({ body: body.value, color: color.value });
  noteId.value = created.id;
  baseline.value = { body: created.body, color: created.color as NoteColor };
}

// Ensure a persisted note exists (for rich actions mid-compose). Returns its id, or
// null when there's nothing to create yet (empty compose — callers just no-op).
async function ensureDraft(): Promise<string | null> {
  if (noteId.value) return noteId.value;
  if (!hasContent.value) return null;
  await createFromFields();
  return noteId.value;
}

// Persist current text fields: create in compose, patch in edit.
async function flush(): Promise<void> {
  if (saving.value) return;
  if (isCreate.value) {
    if (!hasContent.value) return;
    saving.value = true;
    try {
      await createFromFields();
    } finally {
      saving.value = false;
    }
    return;
  }
  const b = baseline.value;
  const nextBody = body.value;
  const changed = nextBody !== b.body || color.value !== b.color;
  if (!changed) return;
  saving.value = true;
  try {
    await notes.saveEdit(noteId.value as string, { body: nextBody, color: color.value });
    baseline.value = { body: nextBody, color: color.value };
  } finally {
    saving.value = false;
  }
}

function resetCompose(): void {
  noteId.value = null;
  setBody("");
  color.value = "default";
  labelList.value = [];
  baseline.value = { body: "", color: "default" };
  uploadError.value = "";
}

// ---- commit / close ----
// Compose only: save the current note and start a fresh one (rapid capture).
async function commitAndContinue(): Promise<void> {
  if (!hasContent.value) return;
  await flush();
  resetCompose();
  await nextTick();
  growAll();
  const first = blocks.value[0];
  await focusBlock(first ? first.id : null);
}
// ---- open/close animation (M7) ----
//
// Owned here rather than by each of the five views that render this component: the
// leave has to play BEFORE the host unmounts us, so the component has to control
// its own visibility and tell the host afterwards.
// Starts TRUE, with `appear` driving the entry animation. Starting false and
// flipping it on mount would be the obvious shape, but the panel lives inside this
// v-if — it wouldn't exist yet to measure.
const visible = ref(true);
const panel = ref<HTMLElement | null>(null);
/** Matches .editor-leave-active in style.css. */
const LEAVE_MS = 140;

onMounted(() => {
  const from = takeMorphOrigin();
  // Grow from the card that was clicked. Set as a transform-origin rather than
  // animating between two rects — see useEditorMorph for why. No origin (compose,
  // or a card that scrolled out of view) simply grows from its own centre.
  if (!from || !panel.value) return;
  // offsetLeft/offsetTop, NOT getBoundingClientRect: the enter-from class has
  // already applied scale(0.94) by now, so the bounding rect is of the SHRUNKEN
  // panel and the origin would land a few pixels off. Offsets are layout geometry
  // and ignore transforms. The offset parent is the inset-0 backdrop, so these are
  // effectively viewport coordinates — which is what the captured point is in.
  panel.value.style.transformOrigin =
    `${from.x - panel.value.offsetLeft}px ${from.y - panel.value.offsetTop}px`;
});

/** Play the leave, then let the host unmount us. */
async function finish(): Promise<void> {
  if (prefersReducedMotion()) {
    emit("close");
    return;
  }
  visible.value = false;
  await new Promise((resolve) => setTimeout(resolve, LEAVE_MS));
  emit("close");
}

// Persist (create in compose, save in edit) and close the editor.
async function close(): Promise<void> {
  await flush();
  await finish();
}
// Esc / click-away DISMISS. A brand-new, not-yet-persisted note is DISCARDED — so an
// accidental keystroke or type-to-compose never litters the board. To keep a new note,
// commit it explicitly (Done, Ctrl/Cmd+Enter, or Shift+Enter). An existing note, or a
// compose already persisted by a rich action, closes normally (saving its text).
async function dismiss(): Promise<void> {
  if (isCreate.value) {
    await finish();
    return;
  }
  await close();
}
function onEsc(): void {
  void dismiss();
}
function onMetaEnter(): void {
  // Ctrl/Cmd+Enter = finish & close, an explicit commit (matches email/chat "send").
  void close();
}
function onBackdropMousedown(): void {
  void dismiss();
}

onMounted(async () => {
  await nextTick();
  growAll();
  // The LAST block, with the caret after its text: opening a note means continuing it,
  // and type-to-compose seeds text that should be typed straight on from.
  const last = blocks.value[blocks.value.length - 1];
  await focusBlock(last ? last.id : null);
});

/** Editing one block: replace its text, leave every other block alone. */
function setText(index: number, text: string): void {
  const out = [...blocks.value];
  out[index] = { ...out[index], text };
  blocks.value = out;
}

function setChecked(index: number, checked: boolean): void {
  const out = [...blocks.value];
  out[index] = { ...out[index], checked };
  blocks.value = out;
}

function onProseInput(index: number, e: Event): void {
  const el = e.target as HTMLTextAreaElement;
  setText(index, el.value);
  grow(el);
}

/** Compose: Shift+Enter saves the note and starts a fresh one (rapid capture). */
function onProseKeydown(e: KeyboardEvent): void {
  if (isCreate.value && e.key === "Enter" && e.shiftKey) {
    e.preventDefault();
    void commitAndContinue();
  }
}

/** Enter on an item makes the next one; on an EMPTY item it ends the list. */
function onTaskEnter(index: number): void {
  const next = afterEnter(blocks.value, index);
  blocks.value = next.blocks;
  void focusBlock(next.focus);
}

/**
 * Backspace at the very start of an EMPTY item removes it.
 *
 * Worth having on the web where Android's is not: a browser sends a real keydown for
 * Backspace, while an Android soft keyboard sends an IME delete that never surfaces as
 * one. Enter-on-empty ends a list on both surfaces; this is the extra way out that
 * only one of them can offer.
 */
function onTaskBackspace(index: number, e: KeyboardEvent): void {
  const el = e.target as HTMLInputElement;
  if (el.value !== "" || el.selectionStart !== 0) return;
  e.preventDefault();
  removeBlock(index);
}

function removeBlock(index: number): void {
  const next = withoutIndex(blocks.value, index);
  blocks.value = next.blocks;
  void focusBlock(next.focus);
}

// ---- reminder ----
const reminderLocal = computed(() => toLocalInput(liveNote.value.remind_at));
async function onReminderChange(e: Event) {
  const id = await ensureDraft();
  if (!id) return;
  await notes.setReminder(id, fromLocalInput((e.target as HTMLInputElement).value));
}
async function onRecurrenceChange(e: Event) {
  const id = await ensureDraft();
  if (!id) return;
  await notes.setRecurrence(id, (e.target as HTMLSelectElement).value || null);
}
async function completeReminder() {
  if (noteId.value) await notes.completeReminder(noteId.value);
}
async function snoozeReminder(minutes: number) {
  if (noteId.value) await notes.snoozeReminder(noteId.value, minutes);
}

// ---- labels ----
async function onLabelsChange(next: NoteLabel[]) {
  const id = await ensureDraft();
  if (!id) return;
  // Tag-sourced labels are governed by the note body, not the picker — always keep
  // them so a picker save can't strip a label the #tag still mandates.
  const tagLabels = labelList.value.filter((lb) => lb.via_tag);
  const tagIds = new Set(tagLabels.map((lb) => lb.id));
  const merged = [...next.filter((lb) => !tagIds.has(lb.id)), ...tagLabels];
  labelList.value = merged;
  await notes.setLabels(
    id,
    merged.map((lb) => lb.id),
  );
}
async function removeLabel(id: string) {
  await onLabelsChange(labelList.value.filter((lb) => lb.id !== id));
}
// Takes the label, not its colour — a tag nobody has coloured derives one from its
// name. Must match NoteCard's chip exactly: the same tag either side of opening a
// note changing colour would be worse than both being grey.
function labelChip(label: { name: string; color: string }): string {
  return LABEL_CHIP_CLASSES[resolveLabelColor(label)] ?? LABEL_CHIP_CLASSES.default;
}

// ---- add a checklist ----
//
// Appends an empty item and puts the caret in it. Unlike every other toolbar button
// this one needs NO persisted note to hang anything off — a checklist is part of the
// body (M304), so it works on an empty compose box the moment it opens.
//
// Appends rather than inserting at the caret because a block editor has no single
// caret to insert at: the field that had focus may not be the one being looked at by
// the time this runs.
function addChecklist(): void {
  const next = plusTask(blocks.value);
  blocks.value = next.blocks;
  void focusBlock(next.focus);
}

// ---- attachments ----
function pickFile() {
  fileInput.value?.click();
}
function attKind(mime: string): "image" | "audio" | "file" {
  if (mime.startsWith("image/")) return "image";
  if (mime.startsWith("audio/")) return "audio";
  return "file";
}
function fmtSize(bytes?: number): string {
  if (!bytes) return "";
  if (bytes < 1024) return `${bytes} B`;
  if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} KB`;
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
}
async function uploadFile(file: File) {
  const id = await ensureDraft();
  if (!id) return;
  uploadError.value = "";
  try {
    await notes.uploadAttachment(id, file);
  } catch (e) {
    uploadError.value = (e as { error?: string }).error ?? "Could not upload file.";
  }
}

// ---- link previews ----
//
// Nothing to trigger any more: the server unfurls a note's URLs in the background
// after each save (`unfurl_queue.py`) and the preview arrives on a later read. What
// is left here is removing one you don't want — the editor is the only place with
// room to offer that, and the card deliberately doesn't.
async function onFileChange(e: Event) {
  const input = e.target as HTMLInputElement;
  const file = input.files?.[0];
  if (file) await uploadFile(file);
  input.value = "";
}
async function onPaste(e: ClipboardEvent) {
  const item = Array.from(e.clipboardData?.items ?? []).find((i) => i.type.startsWith("image/"));
  const file = item?.getAsFile();
  if (file) {
    e.preventDefault();
    await uploadFile(file);
  }
}

// ---- edit-mode lifecycle actions (pin/archive/trash/restore/delete) ----
async function act(fn: () => Promise<void>) {
  await fn();
  await finish();
}

// ---- version history (modal edit only) ----
const showHistory = ref(false);
const revisions = ref<NoteRevision[]>([]);

async function loadRevisions() {
  if (!noteId.value) {
    revisions.value = [];
    return;
  }
  try {
    revisions.value = await notes.fetchRevisions(noteId.value);
  } catch {
    revisions.value = [];
  }
}
function toggleHistory() {
  showHistory.value = !showHistory.value;
  if (showHistory.value) void loadRevisions();
}
async function restoreRevisionAt(revId: string) {
  const id = noteId.value;
  if (!id) return;
  const updated = await notes.restoreRevision(id, revId);
  setBody(updated.body);
  color.value = updated.color;
  baseline.value = { body: updated.body, color: updated.color };
  void loadRevisions(); // the pre-restore state became a new revision
}
function revLabel(iso: string | null): string {
  if (!iso) return "";
  return new Date(iso).toLocaleString(undefined, { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" });
}
function revPreview(rev: NoteRevision): string {
  const s = rev.body.trim().replace(/\s+/g, " ");
  if (!s) return "(empty)";
  return s.length > 80 ? `${s.slice(0, 80)}…` : s;
}
</script>

<template>
  <!-- `appear` because we mount already-open: the host renders us with v-if, so the
       enter has to fire on the first frame rather than on a later state change. -->
  <Transition name="editor" appear>
    <div
      v-if="visible"
      ref="root"
      class="fixed inset-0 z-40 flex items-start justify-center overflow-y-auto bg-black/40 p-4 pt-[10vh]"
      @mousedown.self="onBackdropMousedown"
    >
    <!-- One editor card for BOTH compose (empty note) and edit — a single surface.
         `editor-panel` is the animation's handle: the backdrop only fades, while
         this scales from the card that opened it (see useEditorMorph). -->
    <div
      ref="panel"
      class="editor-panel w-full max-w-lg rounded-xl border border-neutral-200 bg-white shadow-xl dark:border-neutral-700 dark:bg-neutral-900"
      role="dialog"
      aria-modal="true"
      @keydown.esc.stop="onEsc"
      @keydown.enter.meta.prevent="onMetaEnter"
      @keydown.enter.ctrl.prevent="onMetaEnter"
      @paste="onPaste"
    >
      <div class="flex flex-col gap-2 p-4">
        <div v-if="liveNote.attachments.length" class="flex flex-wrap items-center gap-2">
          <template v-for="att in liveNote.attachments" :key="att.id">
            <!-- Image → inline thumbnail -->
            <div v-if="attKind(att.mime) === 'image'" class="group/att relative">
              <img :src="att.url" alt="" loading="lazy" decoding="async" class="h-24 w-24 rounded-lg object-cover" />
              <button
                type="button"
                class="hover-reveal absolute right-1 top-1 rounded-full bg-black/50 px-1.5 text-white opacity-0 transition group-hover/att:opacity-100"
                aria-label="Remove attachment"
                @click="notes.deleteAttachment(liveNote.id, att.id)"
              >
                ×
              </button>
            </div>
            <!-- Audio → inline player -->
            <div
              v-else-if="attKind(att.mime) === 'audio'"
              class="flex items-center gap-2 rounded-lg border border-neutral-200 px-2 py-1 dark:border-neutral-700"
            >
              <audio controls :src="att.url" class="h-8 max-w-[220px]"></audio>
              <button
                type="button"
                class="text-neutral-400 hover:text-red-500"
                aria-label="Remove attachment"
                @click="notes.deleteAttachment(liveNote.id, att.id)"
              >
                ×
              </button>
            </div>
            <!-- Any other file → download chip -->
            <a
              v-else
              :href="att.url"
              download
              class="flex items-center gap-2 rounded-lg border border-neutral-200 px-3 py-2 text-xs hover:bg-neutral-50 dark:border-neutral-700 dark:hover:bg-neutral-800"
            >
              <Icon name="paperclip" />
              <span class="max-w-[160px] truncate">{{ att.filename || "file" }}</span>
              <span v-if="att.size" class="text-neutral-400">{{ fmtSize(att.size) }}</span>
              <button
                type="button"
                class="ml-1 text-neutral-400 hover:text-red-500"
                aria-label="Remove attachment"
                @click.prevent.stop="notes.deleteAttachment(liveNote.id, att.id)"
              >
                ×
              </button>
            </a>
          </template>
        </div>
        <p v-if="uploadError" class="text-xs text-red-600 dark:text-red-400">{{ uploadError }}</p>

        <!-- Fetched automatically after each save; removable here and nowhere else. -->
        <div v-if="liveNote.previews.length" class="flex flex-col gap-2">
          <LinkPreview
            v-for="p in liveNote.previews"
            :key="p.id"
            :preview="p"
            :removable="!liveNote.trashed"
            @remove="notes.deletePreview(liveNote.id, p.id)"
          />
        </div>

        <!-- The body, as fields and checkboxes rather than as markup. A checklist
             item is a real input; a run of prose is one textarea, so typing a
             paragraph still feels like typing a paragraph. -->
        <div class="flex flex-col gap-1">
          <template v-for="(block, i) in blocks" :key="block.id">
            <div v-if="block.checked !== null" class="group/item flex items-center gap-2">
              <input
                type="checkbox"
                class="h-4 w-4 shrink-0 accent-brand"
                :checked="block.checked"
                :aria-label="block.text || 'Checklist item'"
                @change="setChecked(i, ($event.target as HTMLInputElement).checked)"
              />
              <input
                :ref="(el) => setBlockEl(block.id, el)"
                :value="block.text"
                type="text"
                class="min-w-0 flex-1 bg-transparent text-sm leading-relaxed outline-none"
                :class="block.checked ? 'text-neutral-400 line-through' : ''"
                @input="setText(i, ($event.target as HTMLInputElement).value)"
                @keydown.enter.prevent="onTaskEnter(i)"
                @keydown.backspace="onTaskBackspace(i, $event)"
              />
              <button
                type="button"
                class="hover-reveal shrink-0 text-neutral-300 opacity-0 hover:text-neutral-600 focus:opacity-100 group-hover/item:opacity-100 dark:hover:text-neutral-200"
                aria-label="Delete item"
                @click="removeBlock(i)"
              >
                ×
              </button>
            </div>
            <textarea
              v-else
              :ref="(el) => setBlockEl(block.id, el)"
              :value="block.text"
              rows="1"
              :placeholder="i === 0 ? bodyPlaceholder : ''"
              class="w-full resize-none overflow-hidden bg-transparent text-sm leading-relaxed outline-none placeholder:text-neutral-400"
              @input="onProseInput(i, $event)"
              @keydown="onProseKeydown"
            />
          </template>
        </div>
        <!-- No checklist component. The items are lines of the textarea above, which
             is what lets a list sit between two paragraphs (M304). -->

        <div v-if="labelList.length" class="flex flex-wrap gap-1.5 pt-1">
          <span
            v-for="lb in labelList"
            :key="lb.id"
            class="inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs"
            :class="labelChip(lb)"
          >
            {{ lb.via_tag ? "#" + lb.name : lb.name }}
            <button
              v-if="!lb.via_tag"
              type="button"
              class="text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-100"
              :aria-label="`Remove ${lb.name}`"
              @click="removeLabel(lb.id)"
            >
              ×
            </button>
          </span>
        </div>

        <div v-if="richEnabled" class="flex items-center gap-2 pt-1">
          <Icon name="bell" class="text-neutral-400" />
          <input
            type="datetime-local"
            :value="reminderLocal"
            class="rounded-md border border-neutral-300 bg-white px-2 py-1 text-xs text-neutral-700 outline-none [color-scheme:light] focus-visible:ring-2 focus-visible:ring-brand dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-200 dark:[color-scheme:dark]"
            @change="onReminderChange"
          />
          <button
            v-if="liveNote.remind_at"
            type="button"
            class="text-xs text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-200"
            @click="notes.setReminder(liveNote.id, null)"
          >
            Clear
          </button>
        </div>

        <div v-if="richEnabled && liveNote.remind_at" class="flex flex-wrap items-center gap-2 pl-6 text-xs">
          <label class="text-neutral-400">Repeat</label>
          <select
            :value="liveNote.recurrence ?? ''"
            class="rounded-md border border-neutral-300 bg-white px-2 py-1 text-xs text-neutral-700 outline-none focus-visible:ring-2 focus-visible:ring-brand dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-200"
            @change="onRecurrenceChange"
          >
            <option value="">Does not repeat</option>
            <option value="daily">Daily</option>
            <option value="weekly">Weekly</option>
            <option value="monthly">Monthly</option>
            <option value="yearly">Yearly</option>
          </select>
          <button
            type="button"
            class="rounded-md border border-neutral-300 px-2 py-1 text-neutral-600 hover:bg-neutral-100 dark:border-neutral-700 dark:text-neutral-300 dark:hover:bg-neutral-800"
            @click="completeReminder"
          >
            Done
          </button>
          <button type="button" class="text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-200" @click="snoozeReminder(60)">
            Snooze 1h
          </button>
          <button type="button" class="text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-200" @click="snoozeReminder(1440)">
            1d
          </button>
        </div>

        </div>

        <div
          v-if="!isCreate && showHistory"
          class="flex flex-col gap-1 border-t border-neutral-100 pt-2 dark:border-neutral-800"
        >
          <p class="text-xs font-semibold uppercase tracking-wide text-neutral-400">History</p>
          <p v-if="!revisions.length" class="text-xs text-neutral-400">
            No earlier versions yet — your edits will show up here.
          </p>
          <ul v-else class="flex max-h-40 flex-col gap-0.5 overflow-y-auto">
            <li v-for="rev in revisions" :key="rev.id" class="flex items-center gap-2 rounded-md px-1 py-1 text-xs">
              <span class="shrink-0 tabular-nums text-neutral-400">{{ revLabel(rev.created_at) }}</span>
              <span class="min-w-0 flex-1 truncate text-neutral-600 dark:text-neutral-300">{{ revPreview(rev) }}</span>
              <button
                type="button"
                class="shrink-0 rounded px-1.5 py-0.5 font-medium text-brand-700 hover:bg-brand/10 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand dark:text-brand"
                @click="restoreRevisionAt(rev.id)"
              >
                Restore
              </button>
            </li>
          </ul>
        </div>
      </div>

      <div class="flex items-center justify-between gap-2 border-t border-neutral-100 px-3 py-2 dark:border-neutral-800">
        <ColorPicker v-model="color" />
        <div class="flex items-center gap-0.5">
          <button
            v-if="richEnabled && !liveNote.trashed"
            type="button"
            class="icon-btn"
            title="Attach a file"
            aria-label="Attach a file"
            @click="pickFile"
          >
            <Icon name="paperclip" />
          </button>
          <input ref="fileInput" type="file" class="hidden" @change="onFileChange" />
          <button
            v-if="!liveNote.trashed"
            type="button"
            class="icon-btn"
            title="Add a checklist"
            aria-label="Add a checklist"
            @click="addChecklist"
          >
            <Icon name="checkbox" />
          </button>
          <LabelPicker
            v-if="richEnabled && !liveNote.trashed"
            :model-value="labelList"
            @update:model-value="onLabelsChange"
          />
          <button
            v-if="!isCreate"
            type="button"
            class="icon-btn"
            :class="showHistory ? 'text-brand-700 dark:text-brand' : ''"
            title="Version history"
            aria-label="Version history"
            :aria-pressed="showHistory"
            @click="toggleHistory"
          >
            <Icon name="history" />
          </button>
          <template v-if="!isCreate && !liveNote.trashed">
            <button
              type="button"
              class="icon-btn"
              :class="liveNote.pinned ? 'text-brand-700 dark:text-brand' : ''"
              :title="liveNote.pinned ? 'Unpin' : 'Pin'"
              @click="act(() => notes.setPinned(liveNote.id, !liveNote.pinned))"
            >
              <Icon name="pin" />
            </button>
            <button
              type="button"
              class="icon-btn"
              :title="liveNote.archived ? 'Unarchive' : 'Archive'"
              @click="act(() => notes.setArchived(liveNote.id, !liveNote.archived))"
            >
              <Icon name="archive" />
            </button>
            <button
              type="button"
              class="icon-btn"
              title="Move to trash"
              @click="act(() => notes.trash(liveNote.id))"
            >
              <Icon name="trash" />
            </button>
          </template>
          <template v-else-if="!isCreate && liveNote.trashed">
            <button type="button" class="icon-btn" title="Restore" @click="act(() => notes.restore(liveNote.id))">
              <Icon name="restore" />
            </button>
            <button
              type="button"
              class="icon-btn"
              title="Delete forever"
              @click="act(() => notes.deleteForever(liveNote.id))"
            >
              <Icon name="trash" />
            </button>
          </template>
          <button
            type="button"
            :class="
              isCreate
                ? 'rounded-md bg-brand px-3 py-1.5 text-sm font-semibold text-neutral-900 hover:brightness-95 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand disabled:opacity-60'
                : 'rounded-md px-3 py-1.5 text-sm font-semibold text-neutral-700 hover:bg-neutral-100 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand disabled:opacity-60 dark:text-neutral-200 dark:hover:bg-neutral-800'
            "
            :disabled="saving"
            @click="close()"
          >
            {{ isCreate ? "Add note" : "Close" }}
          </button>
        </div>
      </div>
    </div>
  </Transition>
</template>
