@kud/gh-ink 0.10.0 → 0.11.1

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
@@ -20,8 +20,9 @@ type HealthPanelProps = {
20
20
  error: string | null;
21
21
  reload: () => void;
22
22
  onOpenCheck: (check: PrCheck) => void;
23
+ onMerged?: () => void;
23
24
  };
24
- declare const HealthPanel: ({ repo, number, data, error, reload, onOpenCheck, }: HealthPanelProps) => React.JSX.Element;
25
+ declare const HealthPanel: ({ repo, number, data, error, reload, onOpenCheck, onMerged, }: HealthPanelProps) => React.JSX.Element;
25
26
 
26
27
  declare const renderMarkdown: (raw: string, width: number, fileLink?: (path: string, line?: number) => string) => StyledLine[];
27
28
 
@@ -128,6 +129,12 @@ type DetailContext = {
128
129
  onBack: () => void;
129
130
  onRefresh: () => void;
130
131
  onRemove: (item: GHItem) => void;
132
+ /**
133
+ * The PR just merged from here. Returns to the list, marks the row merged for
134
+ * a few seconds, then drops it — distinct from onRemove, which drops it at
135
+ * once. Optional so a host that cannot merge need not implement it.
136
+ */
137
+ onMerged?: (item: GHItem) => void;
131
138
  };
132
139
  type Action = {
133
140
  label: string;
package/dist/index.js CHANGED
@@ -452,7 +452,8 @@ var HealthPanel = ({
452
452
  data,
453
453
  error,
454
454
  reload,
455
- onOpenCheck
455
+ onOpenCheck,
456
+ onMerged
456
457
  }) => {
457
458
  const [cursor, setCursor] = useState(0);
458
459
  const [note, setNote] = useState(null);
@@ -475,13 +476,14 @@ var HealthPanel = ({
475
476
  setNote(msg);
476
477
  setTimeout(() => setNote(null), 2500);
477
478
  };
478
- const act = async (msg, run) => {
479
+ const act = async (msg, run, onSuccess) => {
479
480
  setBusy(true);
480
481
  setNote(`\u22EF ${msg}\u2026`);
481
482
  try {
482
483
  await run();
483
484
  flash(`\u2713 ${msg}`);
484
- reload();
485
+ if (onSuccess) onSuccess();
486
+ else reload();
485
487
  } catch {
486
488
  flash(`\u2717 ${msg} failed`);
487
489
  } finally {
@@ -498,7 +500,7 @@ var HealthPanel = ({
498
500
  if (busy) return;
499
501
  if (confirm === "merge") {
500
502
  if (input === "y" || input === "Y")
501
- void act("merge", () => mergePr(repo, number));
503
+ void act("merge", () => mergePr(repo, number), onMerged);
502
504
  setConfirm(null);
503
505
  return;
504
506
  }
@@ -1446,11 +1448,17 @@ var RepoHeaderRow = ({ repo, gap }) => {
1446
1448
  const fill = Math.max(4, 46 - label.length);
1447
1449
  return /* @__PURE__ */ jsx(Box, { marginTop: gap ? 1 : 0, children: /* @__PURE__ */ jsx(Text2, { dimColor: true, children: " " + label + "\u2500".repeat(fill) }) });
1448
1450
  };
1451
+ var MERGED_HOLD_MS = 3e3;
1452
+ var MERGED_FRAME_MS = 150;
1453
+ var MERGED_FRAMES = ["\u2726", "\u2727", "\u2736", "\u2727"];
1454
+ var MERGED_COLOUR = "#A371F7";
1449
1455
  var ItemRow = ({
1450
1456
  item,
1451
1457
  active,
1452
1458
  gap,
1453
- login
1459
+ login,
1460
+ merged,
1461
+ sparkFrame = 0
1454
1462
  }) => {
1455
1463
  if (item.kind === "repo-header")
1456
1464
  return /* @__PURE__ */ jsx(RepoHeaderRow, { repo: item.repo, gap: gap ?? false });
@@ -1481,16 +1489,20 @@ var ItemRow = ({
1481
1489
  /* @__PURE__ */ jsx(Text2, { bold: active, children: truncate(item.summary, titleMax2) })
1482
1490
  ] });
1483
1491
  }
1484
- const { glyph: icon, color } = healthDisplay[item.health];
1492
+ const { glyph: healthIcon, color: healthColor2 } = healthDisplay[item.health];
1493
+ const icon = merged ? MERGED_FRAMES[sparkFrame % MERGED_FRAMES.length] : healthIcon;
1494
+ const color = merged ? MERGED_COLOUR : healthColor2;
1485
1495
  const [turnIcon, turnColor] = !login || !item.lastActor ? [" ", "white"] : item.lastActor === login ? ["\u2192", "#888888"] : ["\u2190", "#FF8700"];
1486
1496
  const numStr = `#${item.number}`.padEnd(7);
1487
1497
  const showAuthor = !!item.author && item.author !== login;
1488
1498
  const unresolvedLabel = item.unresolved > 0 ? `\uF086 ${item.unresolved}` : "";
1489
1499
  const ageLabel = item.activityAge && item.activityAge !== item.age ? `${item.activityAge} \xB7 ${item.age}` : item.age;
1500
+ const mergedLabel = merged ? "MERGED" : "";
1490
1501
  const suffix = [
1491
1502
  ageLabel || "",
1492
1503
  unresolvedLabel,
1493
- showAuthor ? `by ${item.author}` : ""
1504
+ showAuthor ? `by ${item.author}` : "",
1505
+ mergedLabel
1494
1506
  ].filter(Boolean).join(" ");
1495
1507
  const repoLabel = item.indent ? item.repo : "";
1496
1508
  const fixedWidth = 2 + (item.indent ? 3 : 0) + 2 + 2 + 7 + repoLabel.length + suffix.length + 6;
@@ -1505,7 +1517,8 @@ var ItemRow = ({
1505
1517
  repoLabel ? /* @__PURE__ */ jsx(Text2, { dimColor: true, children: repoLabel }) : null,
1506
1518
  unresolvedLabel ? /* @__PURE__ */ jsx(Text2, { bold: true, color: "#FF8700", children: " " + unresolvedLabel }) : null,
1507
1519
  showAuthor ? /* @__PURE__ */ jsx(Text2, { dimColor: true, italic: true, children: " by " + item.author }) : null,
1508
- ageLabel ? /* @__PURE__ */ jsx(Text2, { dimColor: true, children: " " + ageLabel }) : null
1520
+ ageLabel ? /* @__PURE__ */ jsx(Text2, { dimColor: true, children: " " + ageLabel }) : null,
1521
+ mergedLabel ? /* @__PURE__ */ jsx(Text2, { bold: true, color: MERGED_COLOUR, children: " " + mergedLabel }) : null
1509
1522
  ] });
1510
1523
  };
1511
1524
  var useActionMenu = () => {
@@ -1794,7 +1807,8 @@ var BrowseScreen = ({
1794
1807
  tabHelp,
1795
1808
  isWorkRepo,
1796
1809
  initialIncludeWork,
1797
- brand
1810
+ brand,
1811
+ mergedUrls
1798
1812
  }) => {
1799
1813
  const { rows } = useWindowSize();
1800
1814
  const [includeWork, setIncludeWork] = useState(
@@ -1884,6 +1898,13 @@ var BrowseScreen = ({
1884
1898
  setFlash(msg);
1885
1899
  setTimeout(() => setFlash(null), 2e3);
1886
1900
  };
1901
+ const [sparkFrame, setSparkFrame] = useState(0);
1902
+ const sparkling = (mergedUrls?.length ?? 0) > 0;
1903
+ useEffect(() => {
1904
+ if (!sparkling) return;
1905
+ const id = setInterval(() => setSparkFrame((f) => f + 1), MERGED_FRAME_MS);
1906
+ return () => clearInterval(id);
1907
+ }, [sparkling]);
1887
1908
  useEffect(() => {
1888
1909
  if (refreshError) showFlash(`\u2717 ${refreshError.message}`);
1889
1910
  }, [refreshError]);
@@ -2249,15 +2270,24 @@ var BrowseScreen = ({
2249
2270
  item,
2250
2271
  active: viewStart + i === cursor,
2251
2272
  login,
2252
- gap: viewStart + i > 0 && (item.kind === "repo-header" || item.kind === "subgroup-header" || // A header is always followed by a blank line before its
2253
- // first child — in Other PRs that's "free" because the
2254
- // child is itself a repo-header (gap above). A jira row
2255
- // has no such stand-in, so it needs this explicitly. Never
2256
- // applies between two tickets/PRs only right after a
2257
- // header.
2258
- item.kind === "jira" && ["repo-header", "subgroup-header"].includes(
2259
- section.items[viewStart + i - 1]?.kind
2260
- ))
2273
+ merged: (item.kind === "pr" || item.kind === "issue") && !!mergedUrls?.includes(item.url),
2274
+ sparkFrame,
2275
+ gap: (
2276
+ // Window-relative, never `viewStart + i`. fitCount prices the
2277
+ // window's FIRST row at one line (isFirst), so gapping it when
2278
+ // scrolled draws a row the budget never bought — and the frame
2279
+ // is sized to fill the terminal exactly, so the overflow scrolls
2280
+ // the whole panel up a line instead of clipping.
2281
+ i > 0 && (item.kind === "repo-header" || item.kind === "subgroup-header" || // A header is always followed by a blank line before its
2282
+ // first child — in Other PRs that's "free" because the
2283
+ // child is itself a repo-header (gap above). A jira row
2284
+ // has no such stand-in, so it needs this explicitly. Never
2285
+ // applies between two tickets/PRs — only right after a
2286
+ // header.
2287
+ item.kind === "jira" && ["repo-header", "subgroup-header"].includes(
2288
+ section.items[viewStart + i - 1]?.kind
2289
+ ))
2290
+ )
2261
2291
  },
2262
2292
  `${viewStart + i}:${item.kind === "jira" ? item.instanceKey ?? item.key : item.kind === "repo-header" ? `header:${item.repo}` : item.kind === "subgroup-header" ? `subgroup:${item.label}` : item.kind === "show-more" ? `show-more:${item.hidden[0]?.repo ?? i}` : item.kind === "show-less" ? `show-less:${item.toHide[0]?.repo ?? i}` : `${item.repo}/${item.number}`}`
2263
2293
  )) }),
@@ -2415,6 +2445,9 @@ var App = ({
2415
2445
  clearInterval(id);
2416
2446
  };
2417
2447
  }, [hasCiStatus, ciFetcher, ciPollMs]);
2448
+ const [mergedUrls, setMergedUrls] = useState([]);
2449
+ const mergeTimers = useRef([]);
2450
+ useEffect(() => () => mergeTimers.current.forEach(clearTimeout), []);
2418
2451
  if (state.phase === "loading" || state.phase === "empty" || state.phase === "failed")
2419
2452
  return /* @__PURE__ */ jsxs(
2420
2453
  Box,
@@ -2460,6 +2493,20 @@ var App = ({
2460
2493
  sections: withoutItem(state.sections, target),
2461
2494
  login: state.login
2462
2495
  });
2496
+ const markMerged = (target) => {
2497
+ toBrowse();
2498
+ setMergedUrls(
2499
+ (prev) => prev.includes(target.url) ? prev : [...prev, target.url]
2500
+ );
2501
+ mergeTimers.current.push(
2502
+ setTimeout(() => {
2503
+ setMergedUrls((prev) => prev.filter((u) => u !== target.url));
2504
+ setState(
2505
+ (s) => s.phase === "browse" ? { ...s, sections: withoutItem(s.sections, target) } : s
2506
+ );
2507
+ }, MERGED_HOLD_MS)
2508
+ );
2509
+ };
2463
2510
  return /* @__PURE__ */ jsxs(Fragment, { children: [
2464
2511
  /* @__PURE__ */ jsx(
2465
2512
  BrowseScreen,
@@ -2480,6 +2527,7 @@ var App = ({
2480
2527
  isWorkRepo,
2481
2528
  initialIncludeWork,
2482
2529
  hidden: overlay !== null,
2530
+ mergedUrls,
2483
2531
  ciStatusState: hasCiStatus ? ciStatusState : void 0,
2484
2532
  ciJob,
2485
2533
  tabHelp,
@@ -2511,7 +2559,8 @@ var App = ({
2511
2559
  login: state.login,
2512
2560
  onBack: toBrowse,
2513
2561
  onRefresh: applyOrRefresh,
2514
- onRemove: removeAndReturn
2562
+ onRemove: removeAndReturn,
2563
+ onMerged: markMerged
2515
2564
  }),
2516
2565
  overlay?.kind === "ext" && extensions?.find((e) => e.id === overlay.extId)?.body(toBrowse, overlay.target)
2517
2566
  ] });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kud/gh-ink",
3
- "version": "0.10.0",
3
+ "version": "0.11.1",
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",