@kud/gh-ink 0.35.0 → 0.37.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.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import React, { ReactNode } from 'react';
2
2
  import { PrComments, PrHealthData, PrCheck, Health } from '@kud/gh';
3
- import { StyledLine } from '@kud/ink-ui';
3
+ import { StyledLine, PillVariant } from '@kud/ink-ui';
4
4
 
5
5
  type CommentsPanelProps = {
6
6
  repo: string;
@@ -204,6 +204,20 @@ type TaskRow = {
204
204
  * separately instead of being smuggled past the truncation maths.
205
205
  */
206
206
  note?: string;
207
+ /**
208
+ * A category this row BELONGS to — `epic`, `blocked`, `spike` — drawn as a
209
+ * filled pill after the summary.
210
+ *
211
+ * Deliberately not a second spelling of `note`, which the epic marker used to
212
+ * borrow. The two want opposite weights: a pill says the word itself is the
213
+ * information, while `note` is for a reference the reader follows — a parent
214
+ * ticket key, a source. Filling a breadcrumb gives it a weight it has not
215
+ * earned, and a row can legitimately carry both (a story under someone else's
216
+ * epic shows its parent's key AND, one day, a status of its own).
217
+ */
218
+ pill?: string;
219
+ /** Which fill the pill takes. See `@kud/ink-ui`'s `PillVariant`. */
220
+ pillVariant?: PillVariant;
207
221
  /**
208
222
  * The Jira issue key behind this row, when one exists. Its PRESENCE is what
209
223
  * turns on the ticket affordances — ↵ opens a menu led by `jira issue view`,
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import React2, { createContext, useState, useRef, useEffect, useMemo, useContext } from 'react';
2
2
  import { useWindowSize, useInput, Text, Box } from 'ink';
3
- import { colors, ScrollView, TextInput, LoadingScreen, StatusMessage, FooterHints, useListCursor, Tabs } from '@kud/ink-ui';
3
+ import { colors, ScrollView, TextInput, LoadingScreen, StatusMessage, FooterHints, useListCursor, Tabs, pillWidth, Pill } from '@kud/ink-ui';
4
4
  import { isPassCheck, isFailCheck, isInconclusiveCheck, resolveThread, unresolveThread, replyToThread, rerunFailedRun, mergePr, reRequestReviewer } from '@kud/gh';
5
5
  import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
6
6
  import { $ } from 'zx';
@@ -676,6 +676,9 @@ var invalidateCache = (key) => {
676
676
  };
677
677
 
678
678
  // src/inbox/diff.ts
679
+ var MARK_SEP = "\0";
680
+ var markKey = (sectionId, rowKey) => `${sectionId}${MARK_SEP}${rowKey}`;
681
+ var rowKeyOfMark = (mark) => mark.slice(mark.indexOf(MARK_SEP) + 1);
679
682
  var keyOf = (item) => {
680
683
  if (item.kind === "pr" || item.kind === "issue") return item.url;
681
684
  if (item.kind === "task") {
@@ -707,11 +710,27 @@ var indexRows = (sections) => {
707
710
  for (const section of sections) {
708
711
  section.items.forEach((item, index) => {
709
712
  const key = keyOf(item);
710
- if (key) out.set(key, { item, sectionId: section.id, index });
713
+ if (key)
714
+ out.set(markKey(section.id, key), {
715
+ item,
716
+ sectionId: section.id,
717
+ index
718
+ });
711
719
  });
712
720
  }
713
721
  return out;
714
722
  };
723
+ var rowsOf = (index) => {
724
+ const out = /* @__PURE__ */ new Map();
725
+ for (const [mark, placed] of index) {
726
+ const key = rowKeyOfMark(mark);
727
+ const seen = out.get(key);
728
+ if (seen) seen.add(placed.sectionId);
729
+ else out.set(key, /* @__PURE__ */ new Set([placed.sectionId]));
730
+ }
731
+ return out;
732
+ };
733
+ var sameSections = (a, b) => a.size === b.size && [...a].every((id) => b.has(id));
715
734
  var spliceBack = (next, leaving) => {
716
735
  if (leaving.length === 0) return next;
717
736
  const bySection = /* @__PURE__ */ new Map();
@@ -732,30 +751,43 @@ var spliceBack = (next, leaving) => {
732
751
  var diffSections = (prev, next) => {
733
752
  const before = indexRows(prev);
734
753
  const after = indexRows(next);
754
+ const boardBefore = rowsOf(before);
755
+ const boardAfter = rowsOf(after);
735
756
  const transients = /* @__PURE__ */ new Map();
736
757
  const leaving = [];
737
- for (const [key] of after) if (!before.has(key)) transients.set(key, "in");
738
- for (const [key, was] of before) {
739
- const now = after.get(key);
758
+ for (const [mark] of after)
759
+ if (!before.has(mark))
760
+ transients.set(
761
+ mark,
762
+ boardBefore.has(rowKeyOfMark(mark)) ? "moved-in" : "in"
763
+ );
764
+ for (const [mark, was] of before) {
765
+ const now = after.get(mark);
740
766
  if (!now) {
741
- transients.set(key, "out");
767
+ transients.set(
768
+ mark,
769
+ boardAfter.has(rowKeyOfMark(mark)) ? "moved-out" : "out"
770
+ );
742
771
  leaving.push(was);
743
772
  } else if (renderedState(was.item) !== renderedState(now.item)) {
744
- transients.set(key, "changed");
773
+ transients.set(mark, "changed");
745
774
  }
746
775
  }
747
776
  let added = 0;
748
777
  let removed = 0;
749
- let changed = 0;
750
- for (const [, kind] of transients) {
751
- if (kind === "in") added++;
752
- else if (kind === "out") removed++;
753
- else changed++;
778
+ const changedRows = /* @__PURE__ */ new Set();
779
+ for (const [mark, kind] of transients)
780
+ if (kind === "changed") changedRows.add(rowKeyOfMark(mark));
781
+ for (const [key, sections] of boardAfter) {
782
+ const had = boardBefore.get(key);
783
+ if (!had) added += 1;
784
+ else if (!sameSections(had, sections)) changedRows.add(key);
754
785
  }
786
+ for (const key of boardBefore.keys()) if (!boardAfter.has(key)) removed += 1;
755
787
  return {
756
788
  transients,
757
789
  union: spliceBack(next, leaving),
758
- counts: { added, removed, changed }
790
+ counts: { added, removed, changed: changedRows.size }
759
791
  };
760
792
  };
761
793
  var summariseDiff = (counts) => {
@@ -765,17 +797,19 @@ var summariseDiff = (counts) => {
765
797
  if (counts.changed) parts.push(`${counts.changed} moved`);
766
798
  return parts.join(" \xB7 ");
767
799
  };
768
- var transientOf = (transients, item) => {
800
+ var transientOf = (transients, item, sectionId) => {
769
801
  if (!transients?.size) return void 0;
770
802
  const key = keyOf(item);
771
- return key ? transients.get(key) : void 0;
803
+ return key ? transients.get(markKey(sectionId, key)) : void 0;
772
804
  };
773
805
  var tabsOfMarks = (sections, marks) => {
774
806
  const out = /* @__PURE__ */ new Map();
775
807
  for (const section of sections)
776
808
  for (const item of section.items) {
777
809
  const key = keyOf(item);
778
- if (key && marks.has(key)) out.set(key, section.id);
810
+ if (!key) continue;
811
+ const mark = markKey(section.id, key);
812
+ if (marks.has(mark)) out.set(mark, section.id);
779
813
  }
780
814
  return out;
781
815
  };
@@ -1835,13 +1869,26 @@ var TRANSIT_IN_FRAMES = ["\xB7", "\u25CB", "\u25CE", "\u25C9"];
1835
1869
  var TRANSIT_COLOUR = {
1836
1870
  in: "#3FB950",
1837
1871
  out: "#8B949E",
1838
- changed: "#FF8700"
1872
+ changed: "#FF8700",
1873
+ // A move is neither an arrival nor a departure, so it takes the colour of the
1874
+ // third thing that can happen to a row you already had: it changed. Both ends
1875
+ // of one move wear the same colour and the same word, which is what lets you
1876
+ // recognise the row you just watched leave when you land on the tab it went to.
1877
+ "moved-in": "#FF8700",
1878
+ "moved-out": "#FF8700"
1839
1879
  };
1840
1880
  var TRANSIT_LABEL = {
1841
1881
  in: "NEW",
1842
1882
  out: "GONE",
1843
- changed: "UPDATED"
1844
- };
1883
+ changed: "UPDATED",
1884
+ // Never GONE. The row is still on the board, one tab over, and telling you it
1885
+ // has gone is the marker lying about work you still own — which was the whole
1886
+ // failure: an epic moving tab said nothing at all and simply vanished.
1887
+ "moved-in": "MOVED",
1888
+ "moved-out": "MOVED"
1889
+ };
1890
+ var isDeparture = (t) => t === "out" || t === "moved-out";
1891
+ var isArrival = (t) => t === "in" || t === "moved-in";
1845
1892
  var LEAVING_HOLD_MS = 2500;
1846
1893
  var TAB_MARK = "\u25CF";
1847
1894
  var tabLabel = (label, marked, id) => marked.size === 0 ? label : `${marked.has(id) ? TAB_MARK : " "} ${label}`;
@@ -1881,11 +1928,11 @@ var ItemRow = ({
1881
1928
  ] });
1882
1929
  if (item.kind === "task") {
1883
1930
  const note = item.note ?? "";
1884
- const transitIcon = !transient ? " " : transient === "out" ? TRANSIT_OUT_FRAMES[sparkFrame % TRANSIT_OUT_FRAMES.length] : transient === "in" ? TRANSIT_IN_FRAMES[sparkFrame % TRANSIT_IN_FRAMES.length] : "\u25C9";
1931
+ const transitIcon = !transient ? " " : isDeparture(transient) ? TRANSIT_OUT_FRAMES[sparkFrame % TRANSIT_OUT_FRAMES.length] : isArrival(transient) ? TRANSIT_IN_FRAMES[sparkFrame % TRANSIT_IN_FRAMES.length] : "\u25C9";
1885
1932
  const transitLabel2 = transient ? TRANSIT_LABEL[transient] : "";
1886
1933
  const titleMax2 = Math.max(
1887
1934
  20,
1888
- COLS - item.key.length - note.length - transitLabel2.length - prefix.length - 12
1935
+ COLS - item.key.length - note.length - (item.pill ? pillWidth(item.pill) + 1 : 0) - transitLabel2.length - prefix.length - 12
1889
1936
  );
1890
1937
  return /* @__PURE__ */ jsxs(Box, { marginTop: gap ? 1 : 0, children: [
1891
1938
  /* @__PURE__ */ jsx(Text2, { color: "cyan", children: active ? "\u276F " : " " }),
@@ -1896,18 +1943,22 @@ var ItemRow = ({
1896
1943
  /* @__PURE__ */ jsx(
1897
1944
  Text2,
1898
1945
  {
1899
- bold: active || transient === "in",
1900
- dimColor: transient === "out",
1946
+ bold: active || !!transient && isArrival(transient),
1947
+ dimColor: !!transient && isDeparture(transient),
1901
1948
  strikethrough: transient === "out",
1902
1949
  children: truncate(item.summary, titleMax2)
1903
1950
  }
1904
1951
  ),
1952
+ item.pill ? /* @__PURE__ */ jsxs(Fragment, { children: [
1953
+ /* @__PURE__ */ jsx(Text2, { children: " " }),
1954
+ /* @__PURE__ */ jsx(Pill, { variant: item.pillVariant ?? "muted", children: item.pill })
1955
+ ] }) : null,
1905
1956
  note ? /* @__PURE__ */ jsx(Text2, { dimColor: true, children: ` ${note}` }) : null,
1906
1957
  transitLabel2 ? /* @__PURE__ */ jsx(Text2, { bold: true, color: TRANSIT_COLOUR[transient], children: " " + transitLabel2 }) : null
1907
1958
  ] });
1908
1959
  }
1909
1960
  const { glyph: healthIcon, color: healthColor2 } = healthDisplay[item.health];
1910
- const icon = merged ? MERGED_FRAMES[sparkFrame % MERGED_FRAMES.length] : transient === "out" ? TRANSIT_OUT_FRAMES[sparkFrame % TRANSIT_OUT_FRAMES.length] : transient === "in" ? TRANSIT_IN_FRAMES[sparkFrame % TRANSIT_IN_FRAMES.length] : healthIcon;
1961
+ const icon = merged ? MERGED_FRAMES[sparkFrame % MERGED_FRAMES.length] : transient && isDeparture(transient) ? TRANSIT_OUT_FRAMES[sparkFrame % TRANSIT_OUT_FRAMES.length] : transient && isArrival(transient) ? TRANSIT_IN_FRAMES[sparkFrame % TRANSIT_IN_FRAMES.length] : healthIcon;
1911
1962
  const color = merged ? MERGED_COLOUR : transient ? TRANSIT_COLOUR[transient] : healthColor2;
1912
1963
  const spokeLast = !!login && !!item.lastActor && item.lastActor === login;
1913
1964
  const [turnIcon, turnColor] = item.pinned ? [PIN_MARK, "#FF8700"] : !login || !item.lastActor ? [" ", "white"] : spokeLast ? ["\u2192", "#888888"] : ["\u2190", "#FF8700"];
@@ -1936,8 +1987,8 @@ var ItemRow = ({
1936
1987
  /* @__PURE__ */ jsx(
1937
1988
  Text2,
1938
1989
  {
1939
- bold: active || transient === "in",
1940
- dimColor: transient === "out",
1990
+ bold: active || !!transient && isArrival(transient),
1991
+ dimColor: !!transient && isDeparture(transient),
1941
1992
  strikethrough: transient === "out",
1942
1993
  children: truncate(item.title, titleMax) + " "
1943
1994
  }
@@ -2379,11 +2430,12 @@ var BrowseScreen = ({
2379
2430
  const out = /* @__PURE__ */ new Set();
2380
2431
  if (!transients?.size) return out;
2381
2432
  for (const s of localSections)
2382
- if (s.items.some((item) => transientOf(transients, item))) out.add(s.id);
2433
+ if (s.items.some((item) => transientOf(transients, item, s.id)))
2434
+ out.add(s.id);
2383
2435
  return out;
2384
2436
  }, [localSections, transients]);
2385
2437
  const [sparkFrame, setSparkFrame] = useState(0);
2386
- const sparkling = (mergedUrls?.length ?? 0) > 0 || (leavingUrls?.length ?? 0) > 0 || !!transients?.size && section.items.some((item) => transientOf(transients, item));
2438
+ const sparkling = (mergedUrls?.length ?? 0) > 0 || (leavingUrls?.length ?? 0) > 0 || !!transients?.size && section.items.some((item) => transientOf(transients, item, section.id));
2387
2439
  useEffect(() => {
2388
2440
  if (!sparkling) return;
2389
2441
  const id = setInterval(() => setSparkFrame((f) => f + 1), MERGED_FRAME_MS);
@@ -2853,7 +2905,7 @@ var BrowseScreen = ({
2853
2905
  login,
2854
2906
  merged: (item.kind === "pr" || item.kind === "issue") && !!mergedUrls?.includes(item.url),
2855
2907
  leaving: (item.kind === "pr" || item.kind === "issue") && !!leavingUrls?.includes(item.url),
2856
- transient: transientOf(transients, item),
2908
+ transient: transientOf(transients, item, section.id),
2857
2909
  prefix: treePrefix(section.items, viewStart + i),
2858
2910
  parent: item.kind === "task" && depthOf(section.items[viewStart + i + 1]) > depthOf(item),
2859
2911
  sparkFrame,
@@ -2986,7 +3038,12 @@ var App = ({
2986
3038
  const settleTab = (tabId, marks) => {
2987
3039
  const keys = [...transitTabs.current].filter(([, tab]) => tab === tabId).map(([key]) => key);
2988
3040
  for (const key of keys) transitTabs.current.delete(key);
2989
- const gone = new Set(keys.filter((key) => marks.get(key) === "out"));
3041
+ const gone = new Set(
3042
+ keys.filter((key) => {
3043
+ const mark = marks.get(key);
3044
+ return !!mark && isDeparture(mark);
3045
+ }).map(rowKeyOfMark)
3046
+ );
2990
3047
  setTransients((prev) => {
2991
3048
  const next = new Map(prev);
2992
3049
  for (const key of keys) next.delete(key);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kud/gh-ink",
3
- "version": "0.35.0",
3
+ "version": "0.37.0",
4
4
  "description": "Ink components for rendering GitHub PR review comments and health — controlled, presentation-only, built on @kud/ink-ui and fed by @kud/gh.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -49,7 +49,7 @@
49
49
  },
50
50
  "dependencies": {
51
51
  "@kud/gh": "0.9.0",
52
- "@kud/ink-ui": "0.14.0",
52
+ "@kud/ink-ui": "0.17.0",
53
53
  "zx": "8.8.5"
54
54
  },
55
55
  "devDependencies": {