char-slop/berry

multipurpose self-modifiable agentic harness

git clone https://git.t4t.associates/char-slop/berry

Charlotte Somdeslop: prose pass over ai-generated ui text10b0cd2

main
2.8 KiB91 linesraw
1/** A sidebar link's inline name editor; the input sits beside the link, not inside it. */
2export function conversationName(
3  link: HTMLAnchorElement,
4  label: HTMLElement,
5  save: (title: string | null) => Promise<unknown>,
6) {
7  const editor = document.createElement("input");
8  editor.className = "conversation-name";
9  editor.autocomplete = "off";
10  editor.setAttribute("aria-label", "Conversation name");
11  editor.hidden = true;
12  let title: string | undefined;
13  let fallback = "";
14  let initial = "";
15  let editing = false;
16  let saving = false;
17  let disposed = false;
18  const finish = (focus: boolean) => {
19    editing = false;
20    editor.hidden = true;
21    link.hidden = false;
22    if (focus && !disposed) link.focus();
23  };
24  const start = (event?: Event) => {
25    event?.preventDefault();
26    if (editing || disposed) return;
27    editing = true;
28    initial = title ?? fallback;
29    editor.value = initial;
30    editor.removeAttribute("aria-invalid");
31    editor.title = "save: Enter, cancel: Escape";
32    link.hidden = true;
33    editor.hidden = false;
34    editor.focus();
35    editor.select();
36  };
37  const submit = async (focus: boolean) => {
38    if (!editing || saving || disposed) return;
39    const next = editor.value.trim() || null;
40    if (editor.value === initial || next === (title ?? null)) return finish(focus);
41    const before = title;
42    saving = true;
43    editor.readOnly = true;
44    try {
45      await save(next);
46      if (disposed) return;
47      if (title === before) title = next ?? undefined;
48      label.textContent = title ?? fallback;
49      finish(focus);
50    } catch (error) {
51      if (disposed) return;
52      editor.setAttribute("aria-invalid", "true");
53      editor.title = error instanceof Error ? error.message : String(error);
54      if (focus) editor.focus();
55    } finally {
56      saving = false;
57      editor.readOnly = false;
58    }
59  };
60  let touch = false;
61  link.addEventListener("pointerdown", (event) => {
62    touch = event.pointerType === "touch";
63  });
64  link.addEventListener("dblclick", (event) => {
65    if (!touch && !matchMedia("(pointer: coarse), (hover: none)").matches) start(event);
66  });
67  link.addEventListener("keydown", (event) => {
68    if (event.key === "F2") start(event);
69  });
70  editor.addEventListener("keydown", (event) => {
71    if (event.key !== "Enter" && event.key !== "Escape") return;
72    if (event.isComposing) return;
73    event.preventDefault();
74    if (saving) return;
75    if (event.key === "Escape") finish(true);
76    else void submit(true);
77  });
78  editor.addEventListener("blur", () => void submit(false));
79  return {
80    editor,
81    start,
82    update(next: string | undefined, preview: string) {
83      title = next;
84      fallback = preview;
85      label.textContent = title ?? fallback;
86    },
87    dispose() {
88      disposed = true;
89    },
90  };
91}