taskai-cli 1.6.5__tar.gz → 1.6.6__tar.gz
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.
- {taskai_cli-1.6.5 → taskai_cli-1.6.6}/PKG-INFO +1 -1
- {taskai_cli-1.6.5 → taskai_cli-1.6.6}/pyproject.toml +1 -1
- {taskai_cli-1.6.5 → taskai_cli-1.6.6}/taskai/browser.py +22 -4
- {taskai_cli-1.6.5 → taskai_cli-1.6.6}/taskai/cli.py +8 -1
- {taskai_cli-1.6.5 → taskai_cli-1.6.6}/taskai/static/editpanel.js +159 -6
- {taskai_cli-1.6.5 → taskai_cli-1.6.6}/taskai/static/style.css +139 -14
- {taskai_cli-1.6.5 → taskai_cli-1.6.6}/.github/workflows/publish-to-pypi.yml +0 -0
- {taskai_cli-1.6.5 → taskai_cli-1.6.6}/.gitignore +0 -0
- {taskai_cli-1.6.5 → taskai_cli-1.6.6}/.readthedocs.yaml +0 -0
- {taskai_cli-1.6.5 → taskai_cli-1.6.6}/DEVLOG.md +0 -0
- {taskai_cli-1.6.5 → taskai_cli-1.6.6}/DEVPLAN.md +0 -0
- {taskai_cli-1.6.5 → taskai_cli-1.6.6}/README.md +0 -0
- {taskai_cli-1.6.5 → taskai_cli-1.6.6}/TRIAGE.md +0 -0
- {taskai_cli-1.6.5 → taskai_cli-1.6.6}/docs/.gitignore +0 -0
- {taskai_cli-1.6.5 → taskai_cli-1.6.6}/docs/_static/.gitkeep +0 -0
- {taskai_cli-1.6.5 → taskai_cli-1.6.6}/docs/ai-integration.md +0 -0
- {taskai_cli-1.6.5 → taskai_cli-1.6.6}/docs/browser-mode.md +0 -0
- {taskai_cli-1.6.5 → taskai_cli-1.6.6}/docs/commands.md +0 -0
- {taskai_cli-1.6.5 → taskai_cli-1.6.6}/docs/conf.py +0 -0
- {taskai_cli-1.6.5 → taskai_cli-1.6.6}/docs/configuration.md +0 -0
- {taskai_cli-1.6.5 → taskai_cli-1.6.6}/docs/getting-started.md +0 -0
- {taskai_cli-1.6.5 → taskai_cli-1.6.6}/docs/index.md +0 -0
- {taskai_cli-1.6.5 → taskai_cli-1.6.6}/docs/interactive-mode.md +0 -0
- {taskai_cli-1.6.5 → taskai_cli-1.6.6}/docs/item-data.md +0 -0
- {taskai_cli-1.6.5 → taskai_cli-1.6.6}/docs/managing-the-tree.md +0 -0
- {taskai_cli-1.6.5 → taskai_cli-1.6.6}/docs/requirements.txt +0 -0
- {taskai_cli-1.6.5 → taskai_cli-1.6.6}/scripts/bump_version.py +0 -0
- {taskai_cli-1.6.5 → taskai_cli-1.6.6}/taskai/config.py +0 -0
- {taskai_cli-1.6.5 → taskai_cli-1.6.6}/taskai/errors.py +0 -0
- {taskai_cli-1.6.5 → taskai_cli-1.6.6}/taskai/help_menu.py +0 -0
- {taskai_cli-1.6.5 → taskai_cli-1.6.6}/taskai/json_dir_database.py +0 -0
- {taskai_cli-1.6.5 → taskai_cli-1.6.6}/taskai/llm_models.py +0 -0
- {taskai_cli-1.6.5 → taskai_cli-1.6.6}/taskai/models.py +0 -0
- {taskai_cli-1.6.5 → taskai_cli-1.6.6}/taskai/services/ai.py +0 -0
- {taskai_cli-1.6.5 → taskai_cli-1.6.6}/taskai/services/pomodoro.py +0 -0
- {taskai_cli-1.6.5 → taskai_cli-1.6.6}/taskai/services/repair_database.py +0 -0
- {taskai_cli-1.6.5 → taskai_cli-1.6.6}/taskai/services/user_setup.py +0 -0
- {taskai_cli-1.6.5 → taskai_cli-1.6.6}/taskai/static/canvas.js +0 -0
- {taskai_cli-1.6.5 → taskai_cli-1.6.6}/taskai/static/console.js +0 -0
- {taskai_cli-1.6.5 → taskai_cli-1.6.6}/taskai/static/index.html +0 -0
- {taskai_cli-1.6.5 → taskai_cli-1.6.6}/taskai/views.py +0 -0
- {taskai_cli-1.6.5 → taskai_cli-1.6.6}/test/test_cli.py +0 -0
- {taskai_cli-1.6.5 → taskai_cli-1.6.6}/test/test_execution.py +0 -0
- {taskai_cli-1.6.5 → taskai_cli-1.6.6}/test/test_json_dir_database.py +0 -0
- {taskai_cli-1.6.5 → taskai_cli-1.6.6}/test/test_view.py +0 -0
|
@@ -24,11 +24,29 @@ def index():
|
|
|
24
24
|
return FileResponse(os.path.join(static_abs_dir, "index.html"))
|
|
25
25
|
|
|
26
26
|
|
|
27
|
+
def _resolve_comments(item):
|
|
28
|
+
"""Full comment objects for an item, in comment_ids order. Skips any
|
|
29
|
+
dangling id rather than 500-ing the whole tree (id-lists are maintained
|
|
30
|
+
by hand on both sides, so a desync is possible)."""
|
|
31
|
+
comments = []
|
|
32
|
+
for cid in item.comment_ids:
|
|
33
|
+
try:
|
|
34
|
+
comments.append(db.get_comment(cid).model_dump(mode="json"))
|
|
35
|
+
except Exception:
|
|
36
|
+
continue
|
|
37
|
+
return comments
|
|
38
|
+
|
|
39
|
+
|
|
27
40
|
def _full_tree():
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
41
|
+
tree = {}
|
|
42
|
+
for item_id in db.get_item_ids():
|
|
43
|
+
item = db.get_item(item_id)
|
|
44
|
+
dump = item.model_dump(mode="json")
|
|
45
|
+
# not a TodoItem field — resolved here so the frontend gets comment
|
|
46
|
+
# text from the same /api/tree read as everything else
|
|
47
|
+
dump["comments"] = _resolve_comments(item)
|
|
48
|
+
tree[item_id] = dump
|
|
49
|
+
return tree
|
|
32
50
|
|
|
33
51
|
|
|
34
52
|
@app.get("/api/tree")
|
|
@@ -92,7 +92,14 @@ class Controller:
|
|
|
92
92
|
if v is None:
|
|
93
93
|
continue
|
|
94
94
|
match k:
|
|
95
|
-
case "completed":
|
|
95
|
+
case "completed":
|
|
96
|
+
# note: bool("false") is True — parse the string, don't
|
|
97
|
+
# cast it. Real bools (the recursive re-parse, and the
|
|
98
|
+
# complete/done/undone dispatch) pass straight through.
|
|
99
|
+
if isinstance(v, bool):
|
|
100
|
+
kwargs["completed"] = v
|
|
101
|
+
else:
|
|
102
|
+
kwargs["completed"] = str(v).strip().lower() in ("true", "1", "yes")
|
|
96
103
|
case "due_by":
|
|
97
104
|
if isinstance(v, str):
|
|
98
105
|
kwargs["due_by"] = datetime.strptime(v, "%m-%d-%Y")
|
|
@@ -7,7 +7,7 @@ const editToggle = document.getElementById("edit-toggle");
|
|
|
7
7
|
const editBody = document.querySelector("#edit-panel .edit-body");
|
|
8
8
|
|
|
9
9
|
const EDIT_PANEL_COLLAPSED_WIDTH = 44;
|
|
10
|
-
const EDIT_PANEL_EXPANDED_WIDTH =
|
|
10
|
+
const EDIT_PANEL_EXPANDED_WIDTH = 448;
|
|
11
11
|
|
|
12
12
|
// reserve the collapsed strip's width in the canvas from the start (instant —
|
|
13
13
|
// there's nothing on screen yet to animate a transition from)
|
|
@@ -36,11 +36,14 @@ function dateInputValueToCliFormat(value) {
|
|
|
36
36
|
// `param` overrides the CLI flag name where it differs from the JSON key.
|
|
37
37
|
const FIELD_DEFS = [
|
|
38
38
|
{ key: "name", label: "Name", type: "text" },
|
|
39
|
-
{ key: "description", label: "Description", type: "textarea" },
|
|
40
39
|
{ key: "status", label: "Status", type: "text" },
|
|
41
40
|
{ key: "priority", label: "Priority", type: "number" },
|
|
42
41
|
{ key: "due_by", label: "Due by", type: "date", format: isoToDateInputValue },
|
|
43
42
|
{ key: "completed", label: "Completed", type: "checkbox" },
|
|
43
|
+
// description goes last and is flagged `grow`: it's the big free-text
|
|
44
|
+
// field, so it sits at the bottom and stretches to fill whatever panel
|
|
45
|
+
// height is left (see .edit-field-grow in style.css)
|
|
46
|
+
{ key: "description", label: "Description", type: "textarea", grow: true },
|
|
44
47
|
];
|
|
45
48
|
|
|
46
49
|
// debounce field updates so we're not POSTing on every keystroke — only
|
|
@@ -76,6 +79,29 @@ async function sendFieldUpdate(item, def, value) {
|
|
|
76
79
|
applyTree(data.tree);
|
|
77
80
|
}
|
|
78
81
|
|
|
82
|
+
// adds a comment via the same command pipeline as everything else —
|
|
83
|
+
// `comment <id> "<text>"`. The returned tree carries the new comment (see
|
|
84
|
+
// _resolve_comments in browser.py), so applyTree refreshes the list.
|
|
85
|
+
async function sendComment(itemId, text) {
|
|
86
|
+
const command = `comment ${itemId} ${quoteArg(text)}`;
|
|
87
|
+
|
|
88
|
+
let data;
|
|
89
|
+
try {
|
|
90
|
+
const res = await fetch("/api/command", {
|
|
91
|
+
method: "POST",
|
|
92
|
+
headers: { "Content-Type": "application/json" },
|
|
93
|
+
body: JSON.stringify({ input: command }),
|
|
94
|
+
});
|
|
95
|
+
data = await res.json();
|
|
96
|
+
} catch (err) {
|
|
97
|
+
appendLine(`Error adding comment to item ${itemId}: ${err.message}`);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (data.output) appendLine(data.output);
|
|
102
|
+
applyTree(data.tree);
|
|
103
|
+
}
|
|
104
|
+
|
|
79
105
|
function handleFieldChange(item, def, value) {
|
|
80
106
|
if (def.type === "checkbox") {
|
|
81
107
|
sendFieldUpdate(item, def, value ? "true" : "false");
|
|
@@ -95,37 +121,49 @@ function handleFieldChange(item, def, value) {
|
|
|
95
121
|
}, FIELD_UPDATE_DEBOUNCE_MS);
|
|
96
122
|
}
|
|
97
123
|
|
|
124
|
+
// the value to show in a field's input for the given item — checkbox uses
|
|
125
|
+
// the boolean directly (assigned to .checked), the rest get a string .value
|
|
126
|
+
function fieldDisplayValue(item, def) {
|
|
127
|
+
const raw = item[def.key];
|
|
128
|
+
if (def.type === "checkbox") return Boolean(raw);
|
|
129
|
+
if (def.type === "textarea") return raw || "";
|
|
130
|
+
if (def.format) return def.format(raw);
|
|
131
|
+
return raw ?? "";
|
|
132
|
+
}
|
|
133
|
+
|
|
98
134
|
function buildField(item, def) {
|
|
99
135
|
const wrapper = document.createElement("label");
|
|
100
136
|
wrapper.className = "edit-field";
|
|
137
|
+
if (def.grow) wrapper.classList.add("edit-field-grow");
|
|
101
138
|
|
|
102
139
|
const labelText = document.createElement("span");
|
|
103
140
|
labelText.className = "edit-field-label";
|
|
104
141
|
labelText.textContent = def.label;
|
|
105
142
|
wrapper.appendChild(labelText);
|
|
106
143
|
|
|
107
|
-
const rawValue = item[def.key];
|
|
108
144
|
let input;
|
|
109
145
|
|
|
110
146
|
if (def.type === "textarea") {
|
|
111
147
|
input = document.createElement("textarea");
|
|
112
|
-
input.value =
|
|
148
|
+
input.value = fieldDisplayValue(item, def);
|
|
113
149
|
wrapper.appendChild(input);
|
|
114
150
|
} else if (def.type === "checkbox") {
|
|
115
151
|
const row = document.createElement("div");
|
|
116
152
|
row.className = "edit-field-checkbox-row";
|
|
117
153
|
input = document.createElement("input");
|
|
118
154
|
input.type = "checkbox";
|
|
119
|
-
input.checked =
|
|
155
|
+
input.checked = fieldDisplayValue(item, def);
|
|
120
156
|
row.appendChild(input);
|
|
121
157
|
wrapper.appendChild(row);
|
|
122
158
|
} else {
|
|
123
159
|
input = document.createElement("input");
|
|
124
160
|
input.type = def.type; // "text", "number", or "date"
|
|
125
|
-
input.value =
|
|
161
|
+
input.value = fieldDisplayValue(item, def);
|
|
126
162
|
wrapper.appendChild(input);
|
|
127
163
|
}
|
|
128
164
|
|
|
165
|
+
input.dataset.fieldKey = def.key; // for the in-place refresh in renderEditForm
|
|
166
|
+
|
|
129
167
|
// checkboxes are a single discrete action (send immediately); everything
|
|
130
168
|
// else listens on "input" so the debounce in handleFieldChange can wait
|
|
131
169
|
// out a pause in typing rather than requiring the field to lose focus
|
|
@@ -138,7 +176,121 @@ function buildField(item, def) {
|
|
|
138
176
|
return wrapper;
|
|
139
177
|
}
|
|
140
178
|
|
|
179
|
+
// "YYYY-MM-DDTHH:MM:SS..." -> "Sep 3, 2026"; empty string if unparseable
|
|
180
|
+
function formatCommentDate(iso) {
|
|
181
|
+
if (!iso) return "";
|
|
182
|
+
const d = new Date(iso);
|
|
183
|
+
if (isNaN(d.getTime())) return "";
|
|
184
|
+
return d.toLocaleDateString(undefined, { month: "short", day: "numeric", year: "numeric" });
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// (re)fill a <ul.comment-list> from an item's resolved `comments` array.
|
|
188
|
+
// Oldest first, newest at the bottom; keeps the view pinned to the bottom
|
|
189
|
+
// (newest) unless the user has deliberately scrolled up into the history.
|
|
190
|
+
function renderCommentList(listEl, comments) {
|
|
191
|
+
const prevScrollTop = listEl.scrollTop;
|
|
192
|
+
const stickToBottom =
|
|
193
|
+
listEl.scrollHeight - listEl.scrollTop - listEl.clientHeight < 8;
|
|
194
|
+
|
|
195
|
+
listEl.innerHTML = "";
|
|
196
|
+
|
|
197
|
+
if (!comments || !comments.length) {
|
|
198
|
+
const empty = document.createElement("li");
|
|
199
|
+
empty.className = "comment-empty";
|
|
200
|
+
empty.textContent = "No comments yet.";
|
|
201
|
+
listEl.appendChild(empty);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
comments.forEach(c => {
|
|
206
|
+
const li = document.createElement("li");
|
|
207
|
+
li.className = "comment-item";
|
|
208
|
+
|
|
209
|
+
// date first so it lands on the left; body second, right-aligned
|
|
210
|
+
const when = formatCommentDate(c.created_on);
|
|
211
|
+
if (when) {
|
|
212
|
+
const time = document.createElement("time");
|
|
213
|
+
time.className = "comment-date";
|
|
214
|
+
time.textContent = when;
|
|
215
|
+
li.appendChild(time);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const body = document.createElement("span");
|
|
219
|
+
body.className = "comment-body";
|
|
220
|
+
body.textContent = c.content;
|
|
221
|
+
li.appendChild(body);
|
|
222
|
+
|
|
223
|
+
listEl.appendChild(li);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// defer to next frame so scrollHeight is valid even on the first render
|
|
227
|
+
// (the <ul> isn't laid out yet when buildCommentsSection calls this)
|
|
228
|
+
if (stickToBottom) {
|
|
229
|
+
requestAnimationFrame(() => { listEl.scrollTop = listEl.scrollHeight; });
|
|
230
|
+
} else {
|
|
231
|
+
listEl.scrollTop = prevScrollTop; // keep the reader where they were
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// read-only comment list + an "add" input, styled like an .edit-field
|
|
236
|
+
function buildCommentsSection(item) {
|
|
237
|
+
const section = document.createElement("div");
|
|
238
|
+
section.className = "edit-field edit-comments";
|
|
239
|
+
|
|
240
|
+
const label = document.createElement("span");
|
|
241
|
+
label.className = "edit-field-label";
|
|
242
|
+
label.textContent = "Comments";
|
|
243
|
+
section.appendChild(label);
|
|
244
|
+
|
|
245
|
+
const list = document.createElement("ul");
|
|
246
|
+
list.className = "comment-list";
|
|
247
|
+
renderCommentList(list, item.comments);
|
|
248
|
+
section.appendChild(list);
|
|
249
|
+
|
|
250
|
+
const addInput = document.createElement("textarea");
|
|
251
|
+
addInput.className = "comment-add-input";
|
|
252
|
+
addInput.rows = 2;
|
|
253
|
+
addInput.placeholder = "Add a comment…";
|
|
254
|
+
// Enter sends, Shift+Enter inserts a newline
|
|
255
|
+
addInput.addEventListener("keydown", e => {
|
|
256
|
+
if (e.key !== "Enter" || e.shiftKey) return;
|
|
257
|
+
e.preventDefault();
|
|
258
|
+
const text = addInput.value.trim();
|
|
259
|
+
if (!text) return;
|
|
260
|
+
addInput.value = "";
|
|
261
|
+
sendComment(item.id, text);
|
|
262
|
+
});
|
|
263
|
+
section.appendChild(addInput);
|
|
264
|
+
|
|
265
|
+
return section;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// id of the item the form is currently showing, so a same-item refresh
|
|
269
|
+
// (e.g. the applyTree after our own debounced field POST) can update values
|
|
270
|
+
// in place instead of tearing the form down and unfocusing the live field
|
|
271
|
+
let renderedItemId = null;
|
|
272
|
+
|
|
273
|
+
// push fresh values into the existing inputs without rebuilding them — skips
|
|
274
|
+
// whichever field the user is currently editing so we don't stomp their input
|
|
275
|
+
function refreshFieldValues(item) {
|
|
276
|
+
editBody.querySelectorAll("[data-field-key]").forEach(input => {
|
|
277
|
+
if (input === document.activeElement) return;
|
|
278
|
+
const def = FIELD_DEFS.find(d => d.key === input.dataset.fieldKey);
|
|
279
|
+
if (!def) return;
|
|
280
|
+
if (def.type === "checkbox") input.checked = fieldDisplayValue(item, def);
|
|
281
|
+
else input.value = fieldDisplayValue(item, def);
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
141
285
|
function renderEditForm(item) {
|
|
286
|
+
if (item && String(item.id) === renderedItemId && editBody.querySelector(".edit-form")) {
|
|
287
|
+
refreshFieldValues(item);
|
|
288
|
+
const list = editBody.querySelector(".comment-list");
|
|
289
|
+
if (list) renderCommentList(list, item.comments);
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
renderedItemId = item ? String(item.id) : null;
|
|
142
294
|
editBody.innerHTML = "";
|
|
143
295
|
|
|
144
296
|
if (!item) {
|
|
@@ -152,6 +304,7 @@ function renderEditForm(item) {
|
|
|
152
304
|
const form = document.createElement("div");
|
|
153
305
|
form.className = "edit-form";
|
|
154
306
|
FIELD_DEFS.forEach(def => form.appendChild(buildField(item, def)));
|
|
307
|
+
form.appendChild(buildCommentsSection(item)); // comment feed at the bottom
|
|
155
308
|
editBody.appendChild(form);
|
|
156
309
|
}
|
|
157
310
|
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
/* console panel heights — also used to keep the edit panel clear of it */
|
|
3
|
+
--console-h-collapsed: 44px;
|
|
4
|
+
--console-h-expanded: 280px;
|
|
5
|
+
--comment-list-h: 200px;
|
|
6
|
+
}
|
|
7
|
+
|
|
1
8
|
html, body {
|
|
2
9
|
margin: 0;
|
|
3
10
|
padding: 0;
|
|
@@ -17,19 +24,19 @@ canvas {
|
|
|
17
24
|
left: 0;
|
|
18
25
|
right: 0;
|
|
19
26
|
bottom: 0;
|
|
20
|
-
z-index: 11; /*
|
|
27
|
+
z-index: 11; /* above the edit panel (which now stops short of it anyway) */
|
|
21
28
|
display: flex;
|
|
22
29
|
flex-direction: column;
|
|
23
30
|
background: #ffffff;
|
|
24
31
|
border-top: 1px solid #e2e4ea;
|
|
25
32
|
box-shadow: 0 -2px 10px rgba(15, 23, 42, 0.06);
|
|
26
|
-
height:
|
|
33
|
+
height: var(--console-h-collapsed);
|
|
27
34
|
overflow: hidden;
|
|
28
35
|
transition: height 0.2s ease;
|
|
29
36
|
}
|
|
30
37
|
|
|
31
38
|
.console-panel.expanded {
|
|
32
|
-
height:
|
|
39
|
+
height: var(--console-h-expanded);
|
|
33
40
|
}
|
|
34
41
|
|
|
35
42
|
.console-toggle {
|
|
@@ -114,14 +121,20 @@ canvas {
|
|
|
114
121
|
transition: width 0.2s ease, height 0.2s ease, background-color 0.2s ease, box-shadow 0.2s ease;
|
|
115
122
|
}
|
|
116
123
|
|
|
124
|
+
/* stop short of the console so the two panels never overlap in the
|
|
125
|
+
bottom-right corner — height tracks the console's collapsed/expanded state */
|
|
117
126
|
.edit-panel.expanded {
|
|
118
|
-
width:
|
|
119
|
-
height: 100vh;
|
|
127
|
+
width: 448px;
|
|
128
|
+
height: calc(100vh - var(--console-h-collapsed));
|
|
120
129
|
background: #ffffff;
|
|
121
130
|
border-left: 1px solid #e2e4ea;
|
|
122
131
|
box-shadow: -2px 0 10px rgba(15, 23, 42, 0.06);
|
|
123
132
|
}
|
|
124
133
|
|
|
134
|
+
body:has(.console-panel.expanded) .edit-panel.expanded {
|
|
135
|
+
height: calc(100vh - var(--console-h-expanded));
|
|
136
|
+
}
|
|
137
|
+
|
|
125
138
|
.edit-toggle {
|
|
126
139
|
display: flex;
|
|
127
140
|
align-items: center;
|
|
@@ -164,20 +177,57 @@ canvas {
|
|
|
164
177
|
.edit-form {
|
|
165
178
|
display: flex;
|
|
166
179
|
flex-direction: column;
|
|
167
|
-
gap:
|
|
168
|
-
padding:
|
|
180
|
+
gap: 10px;
|
|
181
|
+
padding: 12px 14px;
|
|
182
|
+
min-height: 100%;
|
|
183
|
+
box-sizing: border-box;
|
|
169
184
|
}
|
|
170
185
|
|
|
186
|
+
/* label sits inline on the left, control fills the rest — packs the form
|
|
187
|
+
in much tighter vertically than stacked label/control did */
|
|
171
188
|
.edit-field {
|
|
172
189
|
display: flex;
|
|
173
|
-
flex-direction:
|
|
174
|
-
|
|
175
|
-
|
|
190
|
+
flex-direction: row;
|
|
191
|
+
align-items: center;
|
|
192
|
+
gap: 10px;
|
|
193
|
+
font-size: 12px;
|
|
176
194
|
color: #6b7280;
|
|
177
195
|
}
|
|
178
196
|
|
|
179
197
|
.edit-field-label {
|
|
180
198
|
font-weight: 500;
|
|
199
|
+
flex: none;
|
|
200
|
+
width: 76px;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/* description + comments break the row rule: label back on top, full width */
|
|
204
|
+
.edit-field-grow,
|
|
205
|
+
.edit-comments {
|
|
206
|
+
flex-direction: column;
|
|
207
|
+
align-items: stretch;
|
|
208
|
+
gap: 6px;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.edit-field-grow > .edit-field-label,
|
|
212
|
+
.edit-comments > .edit-field-label {
|
|
213
|
+
width: auto;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/* description takes whatever vertical space is left; the comment feed below
|
|
217
|
+
it is a fixed-size block (list scrolls on overflow, add box is a couple
|
|
218
|
+
of lines) so it doesn't fight the description for room */
|
|
219
|
+
.edit-field-grow {
|
|
220
|
+
flex: 1 1 auto;
|
|
221
|
+
min-height: 0;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
.edit-comments {
|
|
225
|
+
flex: none;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.edit-field-grow textarea {
|
|
229
|
+
flex: 1;
|
|
230
|
+
min-height: 96px;
|
|
181
231
|
}
|
|
182
232
|
|
|
183
233
|
.edit-field input[type="text"],
|
|
@@ -189,10 +239,11 @@ canvas {
|
|
|
189
239
|
color: #23252b;
|
|
190
240
|
background: #f5f6f8;
|
|
191
241
|
border: 1px solid #e2e4ea;
|
|
192
|
-
border-radius:
|
|
193
|
-
padding: 8px
|
|
242
|
+
border-radius: 6px;
|
|
243
|
+
padding: 5px 8px;
|
|
194
244
|
box-sizing: border-box;
|
|
195
|
-
|
|
245
|
+
flex: 1 1 auto;
|
|
246
|
+
min-width: 0;
|
|
196
247
|
outline: none;
|
|
197
248
|
}
|
|
198
249
|
|
|
@@ -205,12 +256,13 @@ canvas {
|
|
|
205
256
|
|
|
206
257
|
.edit-field textarea {
|
|
207
258
|
resize: vertical;
|
|
208
|
-
min-height:
|
|
259
|
+
min-height: 52px;
|
|
209
260
|
}
|
|
210
261
|
|
|
211
262
|
.edit-field-checkbox-row {
|
|
212
263
|
display: flex;
|
|
213
264
|
align-items: center;
|
|
265
|
+
flex: 1 1 auto;
|
|
214
266
|
}
|
|
215
267
|
|
|
216
268
|
.edit-field-checkbox-row input[type="checkbox"] {
|
|
@@ -218,3 +270,76 @@ canvas {
|
|
|
218
270
|
height: 16px;
|
|
219
271
|
accent-color: #4772fa;
|
|
220
272
|
}
|
|
273
|
+
|
|
274
|
+
/* comments: a flat read-only feed (no cards — straight on the panel) plus
|
|
275
|
+
an add input, sitting below description */
|
|
276
|
+
.comment-list {
|
|
277
|
+
list-style: none;
|
|
278
|
+
margin: 0;
|
|
279
|
+
padding: 0;
|
|
280
|
+
display: flex;
|
|
281
|
+
flex-direction: column;
|
|
282
|
+
gap: 10px;
|
|
283
|
+
height: var(--comment-list-h);
|
|
284
|
+
overflow-y: auto;
|
|
285
|
+
transition: height 0.2s ease;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/* when the console is open the edit panel is shorter — split that loss
|
|
289
|
+
evenly between the description box and the comment list rather than
|
|
290
|
+
taking it all out of the description */
|
|
291
|
+
body:has(.console-panel.expanded) .comment-list {
|
|
292
|
+
height: calc(
|
|
293
|
+
var(--comment-list-h)
|
|
294
|
+
- (var(--console-h-expanded) - var(--console-h-collapsed)) / 2
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/* date pinned left, comment text pinned right and wrapping under itself */
|
|
299
|
+
.comment-item {
|
|
300
|
+
display: flex;
|
|
301
|
+
gap: 12px;
|
|
302
|
+
align-items: baseline;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
.comment-date {
|
|
306
|
+
flex: none;
|
|
307
|
+
font-size: 11px;
|
|
308
|
+
color: #9aa0ab;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
.comment-body {
|
|
312
|
+
flex: 1;
|
|
313
|
+
min-width: 0;
|
|
314
|
+
text-align: right;
|
|
315
|
+
font-size: 12.5px;
|
|
316
|
+
color: #23252b;
|
|
317
|
+
white-space: pre-wrap;
|
|
318
|
+
word-break: break-word;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
.comment-empty {
|
|
322
|
+
font-size: 12px;
|
|
323
|
+
color: #9aa0ab;
|
|
324
|
+
font-style: italic;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
.comment-add-input {
|
|
328
|
+
flex: none;
|
|
329
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
330
|
+
font-size: 13px;
|
|
331
|
+
color: #23252b;
|
|
332
|
+
background: #f5f6f8;
|
|
333
|
+
border: 1px solid #e2e4ea;
|
|
334
|
+
border-radius: 6px;
|
|
335
|
+
padding: 5px 8px;
|
|
336
|
+
box-sizing: border-box;
|
|
337
|
+
width: 100%;
|
|
338
|
+
height: 52px;
|
|
339
|
+
resize: none;
|
|
340
|
+
outline: none;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
.comment-add-input:focus {
|
|
344
|
+
border-color: #4772fa;
|
|
345
|
+
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|