@starlove/orbit 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,155 @@
1
+ import { cleanText, escapeHtml, renderPreservedText } from "./format.js";
2
+
3
+ const NOTE_FALLBACK = "No board notes yet";
4
+ const NOTES_PLACEHOLDER = "Add board notes for yourself and your agents…";
5
+
6
+ function compactText(value) {
7
+ return cleanText(value || "")
8
+ .split("\n")
9
+ .map((line) => line.trim())
10
+ .filter(Boolean)
11
+ .join(" ")
12
+ .replace(/\s+/g, " ")
13
+ .trim();
14
+ }
15
+
16
+ export function boardNotesSummary(board = {}) {
17
+ return compactText(board.project_notes) || NOTE_FALLBACK;
18
+ }
19
+
20
+ function updateCachedBoard(board, appState) {
21
+ if (!board?.id || !appState?.data?.boards) return;
22
+ const index = appState.data.boards.findIndex((item) => item.id === board.id);
23
+ if (index >= 0) appState.data.boards.splice(index, 1, { ...appState.data.boards[index], ...board });
24
+ }
25
+
26
+ let lastRenderedBoard = {};
27
+
28
+ function renderNotesBody(board) {
29
+ const notes = board?.project_notes || "";
30
+ const hasNotes = Boolean(notes.trim());
31
+ const placeholderClass = hasNotes ? "" : " is-placeholder";
32
+ const inner = hasNotes ? renderPreservedText(notes) : escapeHtml(NOTES_PLACEHOLDER);
33
+ return `
34
+ <div
35
+ class="inline-md-field-body preserved-text-body editable-field brand-focus-notes-edit${placeholderClass}"
36
+ data-edit-field="project_notes"
37
+ tabindex="0"
38
+ title="Click to edit"
39
+ role="button"
40
+ aria-label="Edit board notes"
41
+ >${inner}</div>
42
+ `;
43
+ }
44
+
45
+ export function renderTopbarFocus(board = lastRenderedBoard) {
46
+ lastRenderedBoard = board || {};
47
+ const summary = boardNotesSummary(lastRenderedBoard);
48
+ const notesButton = document.getElementById("brandNotesBtn");
49
+ const notesText = document.getElementById("brandNotesText");
50
+ const notesBody = document.getElementById("brandFocusNotes");
51
+
52
+ if (notesButton) {
53
+ notesButton.hidden = false;
54
+ notesButton.dataset.empty = summary === NOTE_FALLBACK ? "true" : "false";
55
+ notesButton.setAttribute("aria-label", `Show board notes: ${summary}`);
56
+ }
57
+ if (notesText) notesText.textContent = summary === NOTE_FALLBACK ? "" : summary;
58
+ if (notesBody) notesBody.innerHTML = renderNotesBody(lastRenderedBoard);
59
+ }
60
+
61
+ function setPopoverOpen(open) {
62
+ const buttons = [
63
+ document.getElementById("brandFocusBtn"),
64
+ document.getElementById("brandNotesBtn")
65
+ ].filter(Boolean);
66
+ const popover = document.getElementById("brandFocusPopover");
67
+ if (!popover) return;
68
+ for (const button of buttons) {
69
+ button.setAttribute("aria-expanded", open ? "true" : "false");
70
+ button.dataset.state = open ? "open" : "closed";
71
+ }
72
+ popover.hidden = !open;
73
+ }
74
+
75
+ function togglePopover() {
76
+ const popover = document.getElementById("brandFocusPopover");
77
+ renderTopbarFocus();
78
+ setPopoverOpen(Boolean(popover?.hidden));
79
+ }
80
+
81
+ async function startBoardNotesEdit(sourceEvent) {
82
+ const notesEl = document.querySelector('#brandFocusPopover [data-edit-field="project_notes"]');
83
+ if (!notesEl || !lastRenderedBoard?.id) return;
84
+ const { startInlineEdit } = await import("./ticket-detail.js");
85
+ startInlineEdit(notesEl, {
86
+ fieldName: "project_notes",
87
+ multiline: true,
88
+ initialValue: lastRenderedBoard.project_notes || "",
89
+ ariaLabel: "Edit board notes",
90
+ sourceEvent,
91
+ rerender: async () => {
92
+ renderTopbarFocus(lastRenderedBoard);
93
+ setPopoverOpen(true);
94
+ },
95
+ commit: async (next) => {
96
+ const [{ api }, { state }] = await Promise.all([
97
+ import("./api.js"),
98
+ import("./state.js")
99
+ ]);
100
+ const updatedBoard = await api(`/api/boards/${encodeURIComponent(lastRenderedBoard.id || state.boardId)}`, {
101
+ method: "PATCH",
102
+ body: { project_notes: cleanText(next) }
103
+ });
104
+ lastRenderedBoard = { ...lastRenderedBoard, ...updatedBoard };
105
+ updateCachedBoard(lastRenderedBoard, state);
106
+ renderTopbarFocus(lastRenderedBoard);
107
+ setPopoverOpen(true);
108
+ }
109
+ });
110
+ }
111
+
112
+ export function wireTopbarFocus() {
113
+ const logoButton = document.getElementById("brandFocusBtn");
114
+ const notesButton = document.getElementById("brandNotesBtn");
115
+ const popover = document.getElementById("brandFocusPopover");
116
+ if (!logoButton || !notesButton || !popover) return;
117
+
118
+ logoButton.addEventListener("click", (event) => {
119
+ event.stopPropagation();
120
+ togglePopover();
121
+ });
122
+
123
+ notesButton.addEventListener("click", (event) => {
124
+ event.stopPropagation();
125
+ togglePopover();
126
+ });
127
+
128
+ popover.addEventListener("click", (event) => {
129
+ event.stopPropagation();
130
+ if (event.target.closest('[data-edit-field="project_notes"]')) {
131
+ void startBoardNotesEdit(event);
132
+ }
133
+ });
134
+
135
+ popover.addEventListener("keydown", (event) => {
136
+ if (event.key !== "Enter" && event.key !== " ") return;
137
+ if (!event.target.closest('[data-edit-field="project_notes"]')) return;
138
+ event.preventDefault();
139
+ void startBoardNotesEdit(event);
140
+ });
141
+
142
+ document.addEventListener("click", (event) => {
143
+ if (
144
+ event.target.closest("#brandFocusBtn") ||
145
+ event.target.closest("#brandNotesBtn") ||
146
+ event.target.closest("#brandFocusPopover")
147
+ ) return;
148
+ setPopoverOpen(false);
149
+ });
150
+
151
+ document.addEventListener("keydown", (event) => {
152
+ if (event.key !== "Escape") return;
153
+ setPopoverOpen(false);
154
+ });
155
+ }