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.
Files changed (45) hide show
  1. {taskai_cli-1.6.5 → taskai_cli-1.6.6}/PKG-INFO +1 -1
  2. {taskai_cli-1.6.5 → taskai_cli-1.6.6}/pyproject.toml +1 -1
  3. {taskai_cli-1.6.5 → taskai_cli-1.6.6}/taskai/browser.py +22 -4
  4. {taskai_cli-1.6.5 → taskai_cli-1.6.6}/taskai/cli.py +8 -1
  5. {taskai_cli-1.6.5 → taskai_cli-1.6.6}/taskai/static/editpanel.js +159 -6
  6. {taskai_cli-1.6.5 → taskai_cli-1.6.6}/taskai/static/style.css +139 -14
  7. {taskai_cli-1.6.5 → taskai_cli-1.6.6}/.github/workflows/publish-to-pypi.yml +0 -0
  8. {taskai_cli-1.6.5 → taskai_cli-1.6.6}/.gitignore +0 -0
  9. {taskai_cli-1.6.5 → taskai_cli-1.6.6}/.readthedocs.yaml +0 -0
  10. {taskai_cli-1.6.5 → taskai_cli-1.6.6}/DEVLOG.md +0 -0
  11. {taskai_cli-1.6.5 → taskai_cli-1.6.6}/DEVPLAN.md +0 -0
  12. {taskai_cli-1.6.5 → taskai_cli-1.6.6}/README.md +0 -0
  13. {taskai_cli-1.6.5 → taskai_cli-1.6.6}/TRIAGE.md +0 -0
  14. {taskai_cli-1.6.5 → taskai_cli-1.6.6}/docs/.gitignore +0 -0
  15. {taskai_cli-1.6.5 → taskai_cli-1.6.6}/docs/_static/.gitkeep +0 -0
  16. {taskai_cli-1.6.5 → taskai_cli-1.6.6}/docs/ai-integration.md +0 -0
  17. {taskai_cli-1.6.5 → taskai_cli-1.6.6}/docs/browser-mode.md +0 -0
  18. {taskai_cli-1.6.5 → taskai_cli-1.6.6}/docs/commands.md +0 -0
  19. {taskai_cli-1.6.5 → taskai_cli-1.6.6}/docs/conf.py +0 -0
  20. {taskai_cli-1.6.5 → taskai_cli-1.6.6}/docs/configuration.md +0 -0
  21. {taskai_cli-1.6.5 → taskai_cli-1.6.6}/docs/getting-started.md +0 -0
  22. {taskai_cli-1.6.5 → taskai_cli-1.6.6}/docs/index.md +0 -0
  23. {taskai_cli-1.6.5 → taskai_cli-1.6.6}/docs/interactive-mode.md +0 -0
  24. {taskai_cli-1.6.5 → taskai_cli-1.6.6}/docs/item-data.md +0 -0
  25. {taskai_cli-1.6.5 → taskai_cli-1.6.6}/docs/managing-the-tree.md +0 -0
  26. {taskai_cli-1.6.5 → taskai_cli-1.6.6}/docs/requirements.txt +0 -0
  27. {taskai_cli-1.6.5 → taskai_cli-1.6.6}/scripts/bump_version.py +0 -0
  28. {taskai_cli-1.6.5 → taskai_cli-1.6.6}/taskai/config.py +0 -0
  29. {taskai_cli-1.6.5 → taskai_cli-1.6.6}/taskai/errors.py +0 -0
  30. {taskai_cli-1.6.5 → taskai_cli-1.6.6}/taskai/help_menu.py +0 -0
  31. {taskai_cli-1.6.5 → taskai_cli-1.6.6}/taskai/json_dir_database.py +0 -0
  32. {taskai_cli-1.6.5 → taskai_cli-1.6.6}/taskai/llm_models.py +0 -0
  33. {taskai_cli-1.6.5 → taskai_cli-1.6.6}/taskai/models.py +0 -0
  34. {taskai_cli-1.6.5 → taskai_cli-1.6.6}/taskai/services/ai.py +0 -0
  35. {taskai_cli-1.6.5 → taskai_cli-1.6.6}/taskai/services/pomodoro.py +0 -0
  36. {taskai_cli-1.6.5 → taskai_cli-1.6.6}/taskai/services/repair_database.py +0 -0
  37. {taskai_cli-1.6.5 → taskai_cli-1.6.6}/taskai/services/user_setup.py +0 -0
  38. {taskai_cli-1.6.5 → taskai_cli-1.6.6}/taskai/static/canvas.js +0 -0
  39. {taskai_cli-1.6.5 → taskai_cli-1.6.6}/taskai/static/console.js +0 -0
  40. {taskai_cli-1.6.5 → taskai_cli-1.6.6}/taskai/static/index.html +0 -0
  41. {taskai_cli-1.6.5 → taskai_cli-1.6.6}/taskai/views.py +0 -0
  42. {taskai_cli-1.6.5 → taskai_cli-1.6.6}/test/test_cli.py +0 -0
  43. {taskai_cli-1.6.5 → taskai_cli-1.6.6}/test/test_execution.py +0 -0
  44. {taskai_cli-1.6.5 → taskai_cli-1.6.6}/test/test_json_dir_database.py +0 -0
  45. {taskai_cli-1.6.5 → taskai_cli-1.6.6}/test/test_view.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: taskai-cli
3
- Version: 1.6.5
3
+ Version: 1.6.6
4
4
  Author-email: Alex Paskal <alexcpaskal@gmail.com>
5
5
  Requires-Python: >=3.12
6
6
  Requires-Dist: fastapi>=0.141.1
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "taskai-cli"
3
- version = "1.6.5"
3
+ version = "1.6.6"
4
4
  description = ""
5
5
  authors = [{name = "Alex Paskal", email = "alexcpaskal@gmail.com"}]
6
6
  readme = "README.md"
@@ -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
- return {
29
- item_id: db.get_item(item_id).model_dump(mode="json")
30
- for item_id in db.get_item_ids()
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": kwargs["completed"] = bool(v)
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 = 320;
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 = rawValue || "";
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 = Boolean(rawValue);
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 = def.format ? def.format(rawValue) : (rawValue ?? "");
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; /* stay above the edit panel in the shared bottom-right corner */
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: 44px;
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: 280px;
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: 320px;
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: 16px;
168
- padding: 16px;
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: column;
174
- gap: 6px;
175
- font-size: 12.5px;
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: 8px;
193
- padding: 8px 10px;
242
+ border-radius: 6px;
243
+ padding: 5px 8px;
194
244
  box-sizing: border-box;
195
- width: 100%;
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: 60px;
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