@loupekit/sdk 0.13.1 → 0.14.0-next.81

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.
@@ -427,11 +427,35 @@ var Loupe = (() => {
427
427
  .hfeed-p { font-weight: 700; }
428
428
  .hfeed-p-critical { color: var(--pin); }
429
429
  .hfeed-p-high { color: #e0a92c; }
430
+ .hfeed-a { display: flex; gap: 4px; margin-top: 3px; }
431
+ .hfeed-act {
432
+ font-size: 11px; padding: 3px 8px; border: 1px solid var(--line); border-radius: 6px;
433
+ background: var(--bg-3); color: var(--ink); cursor: pointer;
434
+ }
435
+ .hfeed-act:hover { border-color: var(--accent); }
436
+ .hfeed-act.danger:hover, .hfeed-act.danger[data-armed] { border-color: var(--pin); color: var(--pin); }
430
437
  .hempty { padding: 14px; text-align: center; color: var(--muted); font-size: 12px; }
431
438
  .hfoot { margin-top: 14px; padding-top: 10px; border-top: 1px solid var(--line); font-size: 11px; color: var(--muted); text-align: center; }
432
439
 
433
440
  /* Timeline grouping (project scope) + the repo filter. */
434
- .daylabel { margin: 8px 2px 0; font-size: 11px; text-transform: uppercase; letter-spacing: .06em; color: var(--muted); font-weight: 700; }
441
+ .daylabel {
442
+ display: flex; align-items: center; gap: 6px; width: 100%; margin: 8px 0 0; padding: 4px 2px;
443
+ border: 0; background: none; cursor: pointer; text-align: left;
444
+ font-size: 11px; text-transform: uppercase; letter-spacing: .06em; color: var(--muted); font-weight: 700;
445
+ }
446
+ .daylabel::before { content: "\u25BE"; font-size: 10px; width: 10px; }
447
+ .daylabel.shut::before { content: "\u25B8"; }
448
+ .daylabel::after {
449
+ content: attr(data-n); padding: 0 6px; border-radius: 999px; background: var(--bg-3);
450
+ font-size: 10px; letter-spacing: 0;
451
+ }
452
+ .daylabel:hover { color: var(--ink); }
453
+ .listfilters { display: flex; gap: 6px; margin: 6px 0 2px; }
454
+ .listfilters select {
455
+ flex: 1; min-width: 0; font-size: 12px; padding: 4px 6px; border: 1px solid var(--line); border-radius: 7px;
456
+ background: var(--bg-2); color: var(--ink); outline: none; cursor: pointer;
457
+ }
458
+ .listfilters select:focus { border-color: var(--accent); }
435
459
  /* The page a comment belongs to, shown in the project scope (Settings \u2192 Page paths). */
436
460
  .pathtag {
437
461
  padding: 1px 6px; border-radius: 999px; background: var(--bg-3); color: var(--muted);
@@ -3983,7 +4007,7 @@ a.fwdchip { text-decoration: none; cursor: pointer; }
3983
4007
  };
3984
4008
 
3985
4009
  // src/app.ts
3986
- var SDK_VERSION = true ? "0.13.1" : "dev";
4010
+ var SDK_VERSION = true ? "0.14.0" : "dev";
3987
4011
  var FAB_SIZE = 46;
3988
4012
  var FAB_DRAG_THRESHOLD = 6;
3989
4013
  var LAUNCHER_SHORTCUT = "Alt+Shift+L";
@@ -4138,6 +4162,11 @@ a.fwdchip { text-decoration: none; cursor: pointer; }
4138
4162
  this.allLoaded = false;
4139
4163
  /** A clicked stat tile, which narrows the list. */
4140
4164
  this.statFilter = "";
4165
+ /** One board stage to show, or "" for every stage. */
4166
+ this.statusFilter = "";
4167
+ this.sortOrder = "";
4168
+ /** Day groups the reader folded shut. Session only: "Today" means another day tomorrow. */
4169
+ this.collapsedDays = /* @__PURE__ */ new Set();
4141
4170
  /** Accent preset id (see ACCENTS), applied as inline --accent / --accent-soft. */
4142
4171
  this.accent = "indigo";
4143
4172
  /** Collapsed to the one-line minimize bar. */
@@ -4831,6 +4860,27 @@ a.fwdchip { text-decoration: none; cursor: pointer; }
4831
4860
  this.renderList();
4832
4861
  };
4833
4862
  listHead.appendChild(search);
4863
+ const filters = el("div", "listfilters");
4864
+ this.statusSel = el("select", "lf-status");
4865
+ this.statusSel.setAttribute("aria-label", "Filter by status");
4866
+ this.statusSel.append(
4867
+ optionEl("All statuses", "", !this.statusFilter),
4868
+ ...Object.keys(STAGE_LABELS).map((k) => optionEl(STAGE_LABELS[k], k, this.statusFilter === k))
4869
+ );
4870
+ this.statusSel.onchange = () => {
4871
+ this.statusFilter = this.statusSel.value;
4872
+ this.saveState();
4873
+ this.renderList();
4874
+ };
4875
+ this.sortSel = el("select", "lf-sort");
4876
+ this.sortSel.setAttribute("aria-label", "Order");
4877
+ this.sortSel.append(optionEl("Newest first", "newest", false), optionEl("Oldest first", "oldest", false), optionEl("Page order", "page", false));
4878
+ this.sortSel.onchange = () => {
4879
+ this.sortOrder = this.sortSel.value;
4880
+ this.saveState();
4881
+ this.renderList();
4882
+ };
4883
+ filters.append(this.statusSel, this.sortSel);
4834
4884
  this.listEl = el("div", "list");
4835
4885
  const homeView = el("div", "view home-view");
4836
4886
  this.homeEl = homeView;
@@ -4838,7 +4888,7 @@ a.fwdchip { text-decoration: none; cursor: pointer; }
4838
4888
  this.commentsHint = el("div", "hint-slot");
4839
4889
  this.reviewBar = el("div", "reviewbar");
4840
4890
  this.reviewBar.style.display = "none";
4841
- commentsView.append(this.commentsHint, tools, listHead, this.reviewBar, this.listEl, this.buildIntegrations());
4891
+ commentsView.append(this.commentsHint, tools, listHead, filters, this.reviewBar, this.listEl, this.buildIntegrations());
4842
4892
  const activityView = el("div", "view activity-view");
4843
4893
  this.activityEl = activityView;
4844
4894
  const chatView = el("div", "view chat-view");
@@ -5500,17 +5550,49 @@ a.fwdchip { text-decoration: none; cursor: pointer; }
5500
5550
  feed.innerHTML = recent.length ? recent.map((c) => {
5501
5551
  const stage = STAGE_LABELS[normalizeStatus(c.status)];
5502
5552
  const prio = normalizePriority(c.priority);
5503
- return `<button class="hfeed-i" data-id="${escapeAttr(c.id)}"><span class="hfeed-t">${escapeHtml(c.title || (c.body.split("\n")[0] ?? "").slice(0, 60))}</span><span class="hfeed-m">${escapeHtml(c.author?.name ?? "")} \xB7 <span title="${escapeAttr(this.fmtWhen(c.createdAt))}">${fmtAgo(c.createdAt)}</span> \xB7 <span class="hfeed-s hfeed-s-${normalizeStatus(c.status)}">${stage}</span><span class="hfeed-p hfeed-p-${prio}">${PRIORITY_LABELS[prio]}</span></span></button>`;
5553
+ return `<div class="hfeed-i" role="button" tabindex="0" data-id="${escapeAttr(c.id)}"><span class="hfeed-t">${escapeHtml(c.title || (c.body.split("\n")[0] ?? "").slice(0, 60))}</span><span class="hfeed-m">${escapeHtml(c.author?.name ?? "")} \xB7 <span title="${escapeAttr(this.fmtWhen(c.createdAt))}">${fmtAgo(c.createdAt)}</span> \xB7 <span class="hfeed-s hfeed-s-${normalizeStatus(c.status)}">${stage}</span><span class="hfeed-p hfeed-p-${prio}">${PRIORITY_LABELS[prio]}</span></span><span class="hfeed-a"><button class="hfeed-act" data-act="resolve">${isResolved(c) ? "Reopen" : "Resolve"}</button><button class="hfeed-act" data-act="open">Open</button><button class="hfeed-act danger" data-act="delete">Delete</button></span></div>`;
5504
5554
  }).join("") : `<div class="hempty">Nothing here yet.</div>`;
5505
5555
  feed.querySelectorAll(".hfeed-i").forEach((b) => {
5506
- b.onclick = () => {
5507
- const id = b.dataset.id;
5556
+ const id = b.dataset.id;
5557
+ const open = () => {
5508
5558
  this.statFilter = "";
5559
+ this.statusFilter = "";
5509
5560
  this.setTab("comments");
5510
5561
  this.expanded.add(id);
5562
+ const c = recent.find((x) => x.id === id);
5563
+ if (c) void this.loadMessages(c);
5511
5564
  this.renderList();
5512
5565
  this.flash(id);
5513
5566
  };
5567
+ b.onclick = open;
5568
+ b.onkeydown = (e) => {
5569
+ if (e.key === "Enter" || e.key === " ") {
5570
+ e.preventDefault();
5571
+ open();
5572
+ }
5573
+ };
5574
+ b.querySelectorAll(".hfeed-act").forEach((btn) => {
5575
+ btn.onclick = (e) => {
5576
+ e.stopPropagation();
5577
+ const c = recent.find((x) => x.id === id);
5578
+ if (!c) return;
5579
+ if (btn.dataset.act === "open") open();
5580
+ else if (btn.dataset.act === "resolve") void this.toggleResolved(c);
5581
+ else if (btn.dataset.act === "delete") {
5582
+ if (btn.dataset.armed) void this.deleteComment(c);
5583
+ else {
5584
+ btn.dataset.armed = "1";
5585
+ btn.textContent = "Confirm delete";
5586
+ setTimeout(() => {
5587
+ if (btn.isConnected) {
5588
+ delete btn.dataset.armed;
5589
+ btn.textContent = "Delete";
5590
+ }
5591
+ }, 3e3);
5592
+ }
5593
+ }
5594
+ };
5595
+ });
5514
5596
  });
5515
5597
  }
5516
5598
  async loadNotifications() {
@@ -6619,6 +6701,8 @@ a.fwdchip { text-decoration: none; cursor: pointer; }
6619
6701
  if (typeof p?.tab === "string" && known.includes(p.tab)) this.tab = p.tab;
6620
6702
  if (p?.scope === "page" || p?.scope === "all") this.scope = p.scope;
6621
6703
  if (typeof p?.statFilter === "string") this.statFilter = p.statFilter;
6704
+ if (typeof p?.statusFilter === "string" && p.statusFilter in STAGE_LABELS) this.statusFilter = p.statusFilter;
6705
+ if (["newest", "oldest", "page"].includes(p?.sortOrder)) this.sortOrder = p.sortOrder;
6622
6706
  if (p?.float && typeof p.float.w === "number") this.floatRect = { ...this.floatRect, ...p.float };
6623
6707
  if (typeof p?.markersHidden === "boolean") this.markersHidden = p.markersHidden;
6624
6708
  if (typeof p?.launcherHidden === "boolean") this.launcherHidden = p.launcherHidden;
@@ -6645,6 +6729,8 @@ a.fwdchip { text-decoration: none; cursor: pointer; }
6645
6729
  fab: this.fabPos,
6646
6730
  scope: this.scope,
6647
6731
  statFilter: this.statFilter,
6732
+ statusFilter: this.statusFilter,
6733
+ sortOrder: this.sortOrder,
6648
6734
  accent: this.accent,
6649
6735
  minimized: this.minimized,
6650
6736
  hoverHints: this.hoverHints,
@@ -6727,25 +6813,43 @@ a.fwdchip { text-decoration: none; cursor: pointer; }
6727
6813
  if (this.statFilter === "needs_you" && !needsYou({ status: c.status }).needs) return false;
6728
6814
  if (this.statFilter === "resolved" && stage !== "resolved") return false;
6729
6815
  if (this.statFilter === "stale" && (stage === "resolved" || !(Date.parse(c.createdAt) < weekAgo))) return false;
6816
+ if (this.statusFilter && stage !== this.statusFilter) return false;
6730
6817
  return !q || `${c.title ?? ""} ${c.body} ${c.author?.name ?? ""}`.toLowerCase().includes(q);
6731
6818
  });
6732
- if (this.scope === "all") items = [...items].sort((a, b) => b.createdAt.localeCompare(a.createdAt));
6819
+ const order = this.sortOrder || (this.scope === "all" ? "newest" : "page");
6820
+ if (this.sortSel) this.sortSel.value = order;
6821
+ if (this.statusSel) this.statusSel.value = this.statusFilter;
6822
+ if (order === "newest") items = [...items].sort((a, b) => b.createdAt.localeCompare(a.createdAt));
6823
+ if (order === "oldest") items = [...items].sort((a, b) => a.createdAt.localeCompare(b.createdAt));
6824
+ const grouped = order !== "page";
6825
+ const perDay = /* @__PURE__ */ new Map();
6826
+ if (grouped) for (const c of items) perDay.set(dayLabel(c.createdAt), (perDay.get(dayLabel(c.createdAt)) ?? 0) + 1);
6733
6827
  this.renderReviewBar();
6734
6828
  if (!items.length) {
6735
6829
  this.listEl.appendChild(el(
6736
6830
  "div",
6737
6831
  "empty",
6738
- this.statFilter ? "Nothing in this bucket." : q ? "No comments match your search." : this.scope === "all" ? "No feedback in this project yet." : "No comments yet. Use Inspect to pick an element, or Note to drop a comment anywhere on the page."
6832
+ this.statFilter ? "Nothing in this bucket." : this.statusFilter ? `Nothing in ${STAGE_LABELS[this.statusFilter]}.` : q ? "No comments match your search." : this.scope === "all" ? "No feedback in this project yet." : "No comments yet. Use Inspect to pick an element, or Note to drop a comment anywhere on the page."
6739
6833
  ));
6740
6834
  }
6741
6835
  let lastDay = "";
6742
6836
  items.forEach((c, i) => {
6743
- if (this.scope === "all") {
6837
+ if (grouped) {
6744
6838
  const day = dayLabel(c.createdAt);
6839
+ const shut = this.collapsedDays.has(day);
6745
6840
  if (day !== lastDay) {
6746
- this.listEl.appendChild(el("div", "daylabel", day));
6841
+ const head = el("button", "daylabel" + (shut ? " shut" : ""), day);
6842
+ head.dataset.n = String(perDay.get(day) ?? 0);
6843
+ head.setAttribute("aria-expanded", String(!shut));
6844
+ head.onclick = () => {
6845
+ if (shut) this.collapsedDays.delete(day);
6846
+ else this.collapsedDays.add(day);
6847
+ this.renderList();
6848
+ };
6849
+ this.listEl.appendChild(head);
6747
6850
  lastDay = day;
6748
6851
  }
6852
+ if (shut) return;
6749
6853
  }
6750
6854
  this.listEl.appendChild(this.itemView(c, i));
6751
6855
  });
@@ -6892,31 +6996,14 @@ a.fwdchip { text-decoration: none; cursor: pointer; }
6892
6996
  }
6893
6997
  const actions = el("div", "actions");
6894
6998
  const doneBtn = el("button", "", isResolved(c) ? "Reopen" : "Resolve");
6895
- doneBtn.onclick = async (e) => {
6999
+ doneBtn.onclick = (e) => {
6896
7000
  e.stopPropagation();
6897
- const status = isResolved(c) ? "queue" : "resolved";
6898
- c.status = status;
6899
- await this.store.update(c.id, { status });
6900
- this.renderPins();
6901
- this.renderList();
6902
- this.addActivity({
6903
- kind: status === "resolved" ? "comment.resolve" : "comment.reopen",
6904
- label: `${status === "resolved" ? "Resolved" : "Reopened"} \u201C${c.title || c.body.split("\n")[0] || "comment"}\u201D`
6905
- });
7001
+ void this.toggleResolved(c);
6906
7002
  };
6907
7003
  const del = el("button", "", "Delete");
6908
- del.onclick = async (e) => {
7004
+ del.onclick = (e) => {
6909
7005
  e.stopPropagation();
6910
- await this.store.remove(c.id);
6911
- this.comments = this.comments.filter((x) => x.id !== c.id);
6912
- this.resolved.delete(c.id);
6913
- this.renderPins();
6914
- this.renderList();
6915
- this.addActivity({
6916
- kind: "comment.delete",
6917
- label: `Deleted \u201C${c.title || c.body.split("\n")[0] || "comment"}\u201D`,
6918
- level: "warn"
6919
- });
7006
+ void this.deleteComment(c);
6920
7007
  };
6921
7008
  actions.append(doneBtn, del);
6922
7009
  detail.appendChild(actions);
@@ -6933,6 +7020,37 @@ a.fwdchip { text-decoration: none; cursor: pointer; }
6933
7020
  };
6934
7021
  return item;
6935
7022
  }
7023
+ /**
7024
+ * Resolve or reopen a comment. Shared by the card and the Home feed. The page list
7025
+ * and the project list are separate reads, so the change is applied to both.
7026
+ */
7027
+ async toggleResolved(c) {
7028
+ const status = isResolved(c) ? "queue" : "resolved";
7029
+ await this.store.update(c.id, { status });
7030
+ for (const x of [c, ...this.comments, ...this.allComments]) if (x.id === c.id) x.status = status;
7031
+ this.renderPins();
7032
+ this.renderList();
7033
+ this.renderHome();
7034
+ this.addActivity({
7035
+ kind: status === "resolved" ? "comment.resolve" : "comment.reopen",
7036
+ label: `${status === "resolved" ? "Resolved" : "Reopened"} \u201C${c.title || c.body.split("\n")[0] || "comment"}\u201D`
7037
+ });
7038
+ }
7039
+ /** Delete a comment, from the card or the Home feed. */
7040
+ async deleteComment(c) {
7041
+ await this.store.remove(c.id);
7042
+ this.comments = this.comments.filter((x) => x.id !== c.id);
7043
+ this.allComments = this.allComments.filter((x) => x.id !== c.id);
7044
+ this.resolved.delete(c.id);
7045
+ this.renderPins();
7046
+ this.renderList();
7047
+ this.renderHome();
7048
+ this.addActivity({
7049
+ kind: "comment.delete",
7050
+ label: `Deleted \u201C${c.title || c.body.split("\n")[0] || "comment"}\u201D`,
7051
+ level: "warn"
7052
+ });
7053
+ }
6936
7054
  /**
6937
7055
  * The review banner. This is the one place the panel asks a human to decide
6938
7056
  * something, so it sits above everything else in the detail.