@loupekit/sdk 0.13.1 → 0.14.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.
package/dist/index.js CHANGED
@@ -390,11 +390,35 @@ var STYLES = (
390
390
  .hfeed-p { font-weight: 700; }
391
391
  .hfeed-p-critical { color: var(--pin); }
392
392
  .hfeed-p-high { color: #e0a92c; }
393
+ .hfeed-a { display: flex; gap: 4px; margin-top: 3px; }
394
+ .hfeed-act {
395
+ font-size: 11px; padding: 3px 8px; border: 1px solid var(--line); border-radius: 6px;
396
+ background: var(--bg-3); color: var(--ink); cursor: pointer;
397
+ }
398
+ .hfeed-act:hover { border-color: var(--accent); }
399
+ .hfeed-act.danger:hover, .hfeed-act.danger[data-armed] { border-color: var(--pin); color: var(--pin); }
393
400
  .hempty { padding: 14px; text-align: center; color: var(--muted); font-size: 12px; }
394
401
  .hfoot { margin-top: 14px; padding-top: 10px; border-top: 1px solid var(--line); font-size: 11px; color: var(--muted); text-align: center; }
395
402
 
396
403
  /* Timeline grouping (project scope) + the repo filter. */
397
- .daylabel { margin: 8px 2px 0; font-size: 11px; text-transform: uppercase; letter-spacing: .06em; color: var(--muted); font-weight: 700; }
404
+ .daylabel {
405
+ display: flex; align-items: center; gap: 6px; width: 100%; margin: 8px 0 0; padding: 4px 2px;
406
+ border: 0; background: none; cursor: pointer; text-align: left;
407
+ font-size: 11px; text-transform: uppercase; letter-spacing: .06em; color: var(--muted); font-weight: 700;
408
+ }
409
+ .daylabel::before { content: "\u25BE"; font-size: 10px; width: 10px; }
410
+ .daylabel.shut::before { content: "\u25B8"; }
411
+ .daylabel::after {
412
+ content: attr(data-n); padding: 0 6px; border-radius: 999px; background: var(--bg-3);
413
+ font-size: 10px; letter-spacing: 0;
414
+ }
415
+ .daylabel:hover { color: var(--ink); }
416
+ .listfilters { display: flex; gap: 6px; margin: 6px 0 2px; }
417
+ .listfilters select {
418
+ flex: 1; min-width: 0; font-size: 12px; padding: 4px 6px; border: 1px solid var(--line); border-radius: 7px;
419
+ background: var(--bg-2); color: var(--ink); outline: none; cursor: pointer;
420
+ }
421
+ .listfilters select:focus { border-color: var(--accent); }
398
422
  /* The page a comment belongs to, shown in the project scope (Settings \u2192 Page paths). */
399
423
  .pathtag {
400
424
  padding: 1px 6px; border-radius: 999px; background: var(--bg-3); color: var(--muted);
@@ -3946,7 +3970,7 @@ var HttpAdapter = class {
3946
3970
  };
3947
3971
 
3948
3972
  // src/app.ts
3949
- var SDK_VERSION = true ? "0.13.1" : "dev";
3973
+ var SDK_VERSION = true ? "0.14.0" : "dev";
3950
3974
  var FAB_SIZE = 46;
3951
3975
  var FAB_DRAG_THRESHOLD = 6;
3952
3976
  var LAUNCHER_SHORTCUT = "Alt+Shift+L";
@@ -4101,6 +4125,11 @@ var LoupeApp = class {
4101
4125
  this.allLoaded = false;
4102
4126
  /** A clicked stat tile, which narrows the list. */
4103
4127
  this.statFilter = "";
4128
+ /** One board stage to show, or "" for every stage. */
4129
+ this.statusFilter = "";
4130
+ this.sortOrder = "";
4131
+ /** Day groups the reader folded shut. Session only: "Today" means another day tomorrow. */
4132
+ this.collapsedDays = /* @__PURE__ */ new Set();
4104
4133
  /** Accent preset id (see ACCENTS), applied as inline --accent / --accent-soft. */
4105
4134
  this.accent = "indigo";
4106
4135
  /** Collapsed to the one-line minimize bar. */
@@ -4794,6 +4823,27 @@ var LoupeApp = class {
4794
4823
  this.renderList();
4795
4824
  };
4796
4825
  listHead.appendChild(search);
4826
+ const filters = el("div", "listfilters");
4827
+ this.statusSel = el("select", "lf-status");
4828
+ this.statusSel.setAttribute("aria-label", "Filter by status");
4829
+ this.statusSel.append(
4830
+ optionEl("All statuses", "", !this.statusFilter),
4831
+ ...Object.keys(STAGE_LABELS).map((k) => optionEl(STAGE_LABELS[k], k, this.statusFilter === k))
4832
+ );
4833
+ this.statusSel.onchange = () => {
4834
+ this.statusFilter = this.statusSel.value;
4835
+ this.saveState();
4836
+ this.renderList();
4837
+ };
4838
+ this.sortSel = el("select", "lf-sort");
4839
+ this.sortSel.setAttribute("aria-label", "Order");
4840
+ this.sortSel.append(optionEl("Newest first", "newest", false), optionEl("Oldest first", "oldest", false), optionEl("Page order", "page", false));
4841
+ this.sortSel.onchange = () => {
4842
+ this.sortOrder = this.sortSel.value;
4843
+ this.saveState();
4844
+ this.renderList();
4845
+ };
4846
+ filters.append(this.statusSel, this.sortSel);
4797
4847
  this.listEl = el("div", "list");
4798
4848
  const homeView = el("div", "view home-view");
4799
4849
  this.homeEl = homeView;
@@ -4801,7 +4851,7 @@ var LoupeApp = class {
4801
4851
  this.commentsHint = el("div", "hint-slot");
4802
4852
  this.reviewBar = el("div", "reviewbar");
4803
4853
  this.reviewBar.style.display = "none";
4804
- commentsView.append(this.commentsHint, tools, listHead, this.reviewBar, this.listEl, this.buildIntegrations());
4854
+ commentsView.append(this.commentsHint, tools, listHead, filters, this.reviewBar, this.listEl, this.buildIntegrations());
4805
4855
  const activityView = el("div", "view activity-view");
4806
4856
  this.activityEl = activityView;
4807
4857
  const chatView = el("div", "view chat-view");
@@ -5463,17 +5513,49 @@ var LoupeApp = class {
5463
5513
  feed.innerHTML = recent.length ? recent.map((c) => {
5464
5514
  const stage = STAGE_LABELS[normalizeStatus(c.status)];
5465
5515
  const prio = normalizePriority(c.priority);
5466
- 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>`;
5516
+ 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>`;
5467
5517
  }).join("") : `<div class="hempty">Nothing here yet.</div>`;
5468
5518
  feed.querySelectorAll(".hfeed-i").forEach((b) => {
5469
- b.onclick = () => {
5470
- const id = b.dataset.id;
5519
+ const id = b.dataset.id;
5520
+ const open = () => {
5471
5521
  this.statFilter = "";
5522
+ this.statusFilter = "";
5472
5523
  this.setTab("comments");
5473
5524
  this.expanded.add(id);
5525
+ const c = recent.find((x) => x.id === id);
5526
+ if (c) void this.loadMessages(c);
5474
5527
  this.renderList();
5475
5528
  this.flash(id);
5476
5529
  };
5530
+ b.onclick = open;
5531
+ b.onkeydown = (e) => {
5532
+ if (e.key === "Enter" || e.key === " ") {
5533
+ e.preventDefault();
5534
+ open();
5535
+ }
5536
+ };
5537
+ b.querySelectorAll(".hfeed-act").forEach((btn) => {
5538
+ btn.onclick = (e) => {
5539
+ e.stopPropagation();
5540
+ const c = recent.find((x) => x.id === id);
5541
+ if (!c) return;
5542
+ if (btn.dataset.act === "open") open();
5543
+ else if (btn.dataset.act === "resolve") void this.toggleResolved(c);
5544
+ else if (btn.dataset.act === "delete") {
5545
+ if (btn.dataset.armed) void this.deleteComment(c);
5546
+ else {
5547
+ btn.dataset.armed = "1";
5548
+ btn.textContent = "Confirm delete";
5549
+ setTimeout(() => {
5550
+ if (btn.isConnected) {
5551
+ delete btn.dataset.armed;
5552
+ btn.textContent = "Delete";
5553
+ }
5554
+ }, 3e3);
5555
+ }
5556
+ }
5557
+ };
5558
+ });
5477
5559
  });
5478
5560
  }
5479
5561
  async loadNotifications() {
@@ -6582,6 +6664,8 @@ var LoupeApp = class {
6582
6664
  if (typeof p?.tab === "string" && known.includes(p.tab)) this.tab = p.tab;
6583
6665
  if (p?.scope === "page" || p?.scope === "all") this.scope = p.scope;
6584
6666
  if (typeof p?.statFilter === "string") this.statFilter = p.statFilter;
6667
+ if (typeof p?.statusFilter === "string" && p.statusFilter in STAGE_LABELS) this.statusFilter = p.statusFilter;
6668
+ if (["newest", "oldest", "page"].includes(p?.sortOrder)) this.sortOrder = p.sortOrder;
6585
6669
  if (p?.float && typeof p.float.w === "number") this.floatRect = { ...this.floatRect, ...p.float };
6586
6670
  if (typeof p?.markersHidden === "boolean") this.markersHidden = p.markersHidden;
6587
6671
  if (typeof p?.launcherHidden === "boolean") this.launcherHidden = p.launcherHidden;
@@ -6608,6 +6692,8 @@ var LoupeApp = class {
6608
6692
  fab: this.fabPos,
6609
6693
  scope: this.scope,
6610
6694
  statFilter: this.statFilter,
6695
+ statusFilter: this.statusFilter,
6696
+ sortOrder: this.sortOrder,
6611
6697
  accent: this.accent,
6612
6698
  minimized: this.minimized,
6613
6699
  hoverHints: this.hoverHints,
@@ -6690,25 +6776,43 @@ var LoupeApp = class {
6690
6776
  if (this.statFilter === "needs_you" && !needsYou({ status: c.status }).needs) return false;
6691
6777
  if (this.statFilter === "resolved" && stage !== "resolved") return false;
6692
6778
  if (this.statFilter === "stale" && (stage === "resolved" || !(Date.parse(c.createdAt) < weekAgo))) return false;
6779
+ if (this.statusFilter && stage !== this.statusFilter) return false;
6693
6780
  return !q || `${c.title ?? ""} ${c.body} ${c.author?.name ?? ""}`.toLowerCase().includes(q);
6694
6781
  });
6695
- if (this.scope === "all") items = [...items].sort((a, b) => b.createdAt.localeCompare(a.createdAt));
6782
+ const order = this.sortOrder || (this.scope === "all" ? "newest" : "page");
6783
+ if (this.sortSel) this.sortSel.value = order;
6784
+ if (this.statusSel) this.statusSel.value = this.statusFilter;
6785
+ if (order === "newest") items = [...items].sort((a, b) => b.createdAt.localeCompare(a.createdAt));
6786
+ if (order === "oldest") items = [...items].sort((a, b) => a.createdAt.localeCompare(b.createdAt));
6787
+ const grouped = order !== "page";
6788
+ const perDay = /* @__PURE__ */ new Map();
6789
+ if (grouped) for (const c of items) perDay.set(dayLabel(c.createdAt), (perDay.get(dayLabel(c.createdAt)) ?? 0) + 1);
6696
6790
  this.renderReviewBar();
6697
6791
  if (!items.length) {
6698
6792
  this.listEl.appendChild(el(
6699
6793
  "div",
6700
6794
  "empty",
6701
- 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."
6795
+ 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."
6702
6796
  ));
6703
6797
  }
6704
6798
  let lastDay = "";
6705
6799
  items.forEach((c, i) => {
6706
- if (this.scope === "all") {
6800
+ if (grouped) {
6707
6801
  const day = dayLabel(c.createdAt);
6802
+ const shut = this.collapsedDays.has(day);
6708
6803
  if (day !== lastDay) {
6709
- this.listEl.appendChild(el("div", "daylabel", day));
6804
+ const head = el("button", "daylabel" + (shut ? " shut" : ""), day);
6805
+ head.dataset.n = String(perDay.get(day) ?? 0);
6806
+ head.setAttribute("aria-expanded", String(!shut));
6807
+ head.onclick = () => {
6808
+ if (shut) this.collapsedDays.delete(day);
6809
+ else this.collapsedDays.add(day);
6810
+ this.renderList();
6811
+ };
6812
+ this.listEl.appendChild(head);
6710
6813
  lastDay = day;
6711
6814
  }
6815
+ if (shut) return;
6712
6816
  }
6713
6817
  this.listEl.appendChild(this.itemView(c, i));
6714
6818
  });
@@ -6855,31 +6959,14 @@ var LoupeApp = class {
6855
6959
  }
6856
6960
  const actions = el("div", "actions");
6857
6961
  const doneBtn = el("button", "", isResolved(c) ? "Reopen" : "Resolve");
6858
- doneBtn.onclick = async (e) => {
6962
+ doneBtn.onclick = (e) => {
6859
6963
  e.stopPropagation();
6860
- const status = isResolved(c) ? "queue" : "resolved";
6861
- c.status = status;
6862
- await this.store.update(c.id, { status });
6863
- this.renderPins();
6864
- this.renderList();
6865
- this.addActivity({
6866
- kind: status === "resolved" ? "comment.resolve" : "comment.reopen",
6867
- label: `${status === "resolved" ? "Resolved" : "Reopened"} \u201C${c.title || c.body.split("\n")[0] || "comment"}\u201D`
6868
- });
6964
+ void this.toggleResolved(c);
6869
6965
  };
6870
6966
  const del = el("button", "", "Delete");
6871
- del.onclick = async (e) => {
6967
+ del.onclick = (e) => {
6872
6968
  e.stopPropagation();
6873
- await this.store.remove(c.id);
6874
- this.comments = this.comments.filter((x) => x.id !== c.id);
6875
- this.resolved.delete(c.id);
6876
- this.renderPins();
6877
- this.renderList();
6878
- this.addActivity({
6879
- kind: "comment.delete",
6880
- label: `Deleted \u201C${c.title || c.body.split("\n")[0] || "comment"}\u201D`,
6881
- level: "warn"
6882
- });
6969
+ void this.deleteComment(c);
6883
6970
  };
6884
6971
  actions.append(doneBtn, del);
6885
6972
  detail.appendChild(actions);
@@ -6896,6 +6983,37 @@ var LoupeApp = class {
6896
6983
  };
6897
6984
  return item;
6898
6985
  }
6986
+ /**
6987
+ * Resolve or reopen a comment. Shared by the card and the Home feed. The page list
6988
+ * and the project list are separate reads, so the change is applied to both.
6989
+ */
6990
+ async toggleResolved(c) {
6991
+ const status = isResolved(c) ? "queue" : "resolved";
6992
+ await this.store.update(c.id, { status });
6993
+ for (const x of [c, ...this.comments, ...this.allComments]) if (x.id === c.id) x.status = status;
6994
+ this.renderPins();
6995
+ this.renderList();
6996
+ this.renderHome();
6997
+ this.addActivity({
6998
+ kind: status === "resolved" ? "comment.resolve" : "comment.reopen",
6999
+ label: `${status === "resolved" ? "Resolved" : "Reopened"} \u201C${c.title || c.body.split("\n")[0] || "comment"}\u201D`
7000
+ });
7001
+ }
7002
+ /** Delete a comment, from the card or the Home feed. */
7003
+ async deleteComment(c) {
7004
+ await this.store.remove(c.id);
7005
+ this.comments = this.comments.filter((x) => x.id !== c.id);
7006
+ this.allComments = this.allComments.filter((x) => x.id !== c.id);
7007
+ this.resolved.delete(c.id);
7008
+ this.renderPins();
7009
+ this.renderList();
7010
+ this.renderHome();
7011
+ this.addActivity({
7012
+ kind: "comment.delete",
7013
+ label: `Deleted \u201C${c.title || c.body.split("\n")[0] || "comment"}\u201D`,
7014
+ level: "warn"
7015
+ });
7016
+ }
6899
7017
  /**
6900
7018
  * The review banner. This is the one place the panel asks a human to decide
6901
7019
  * something, so it sits above everything else in the detail.