@kud/gh-ink 0.9.1 → 0.11.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
@@ -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
 
@@ -67,6 +68,7 @@ type GHItem = {
67
68
  health: Health;
68
69
  author?: string;
69
70
  age: string;
71
+ activityAge?: string;
70
72
  ts: number;
71
73
  unresolved: number;
72
74
  conversation: number;
@@ -127,6 +129,12 @@ type DetailContext = {
127
129
  onBack: () => void;
128
130
  onRefresh: () => void;
129
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;
130
138
  };
131
139
  type Action = {
132
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
  }
@@ -733,7 +735,9 @@ var repoPriority = (repo) => {
733
735
  };
734
736
  var sortItems = (items) => [...items].sort((a, b) => {
735
737
  const pd = repoPriority(a.repo) - repoPriority(b.repo);
736
- return pd !== 0 ? pd : a.repo.localeCompare(b.repo);
738
+ if (pd !== 0) return pd;
739
+ const rd = a.repo.localeCompare(b.repo);
740
+ return rd !== 0 ? rd : b.ts - a.ts;
737
741
  });
738
742
  var sortByRecency = (items) => [...items].sort((a, b) => b.ts - a.ts);
739
743
  var insertRepoHeaders = (items) => {
@@ -1444,11 +1448,17 @@ var RepoHeaderRow = ({ repo, gap }) => {
1444
1448
  const fill = Math.max(4, 46 - label.length);
1445
1449
  return /* @__PURE__ */ jsx(Box, { marginTop: gap ? 1 : 0, children: /* @__PURE__ */ jsx(Text2, { dimColor: true, children: " " + label + "\u2500".repeat(fill) }) });
1446
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";
1447
1455
  var ItemRow = ({
1448
1456
  item,
1449
1457
  active,
1450
1458
  gap,
1451
- login
1459
+ login,
1460
+ merged,
1461
+ sparkFrame = 0
1452
1462
  }) => {
1453
1463
  if (item.kind === "repo-header")
1454
1464
  return /* @__PURE__ */ jsx(RepoHeaderRow, { repo: item.repo, gap: gap ?? false });
@@ -1479,15 +1489,20 @@ var ItemRow = ({
1479
1489
  /* @__PURE__ */ jsx(Text2, { bold: active, children: truncate(item.summary, titleMax2) })
1480
1490
  ] });
1481
1491
  }
1482
- 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;
1483
1495
  const [turnIcon, turnColor] = !login || !item.lastActor ? [" ", "white"] : item.lastActor === login ? ["\u2192", "#888888"] : ["\u2190", "#FF8700"];
1484
1496
  const numStr = `#${item.number}`.padEnd(7);
1485
1497
  const showAuthor = !!item.author && item.author !== login;
1486
1498
  const unresolvedLabel = item.unresolved > 0 ? `\uF086 ${item.unresolved}` : "";
1499
+ const ageLabel = item.activityAge && item.activityAge !== item.age ? `${item.activityAge} \xB7 ${item.age}` : item.age;
1500
+ const mergedLabel = merged ? "MERGED" : "";
1487
1501
  const suffix = [
1488
- item.age || "",
1502
+ ageLabel || "",
1489
1503
  unresolvedLabel,
1490
- showAuthor ? `by ${item.author}` : ""
1504
+ showAuthor ? `by ${item.author}` : "",
1505
+ mergedLabel
1491
1506
  ].filter(Boolean).join(" ");
1492
1507
  const repoLabel = item.indent ? item.repo : "";
1493
1508
  const fixedWidth = 2 + (item.indent ? 3 : 0) + 2 + 2 + 7 + repoLabel.length + suffix.length + 6;
@@ -1502,7 +1517,8 @@ var ItemRow = ({
1502
1517
  repoLabel ? /* @__PURE__ */ jsx(Text2, { dimColor: true, children: repoLabel }) : null,
1503
1518
  unresolvedLabel ? /* @__PURE__ */ jsx(Text2, { bold: true, color: "#FF8700", children: " " + unresolvedLabel }) : null,
1504
1519
  showAuthor ? /* @__PURE__ */ jsx(Text2, { dimColor: true, italic: true, children: " by " + item.author }) : null,
1505
- item.age ? /* @__PURE__ */ jsx(Text2, { dimColor: true, children: " " + item.age }) : null
1520
+ ageLabel ? /* @__PURE__ */ jsx(Text2, { dimColor: true, children: " " + ageLabel }) : null,
1521
+ mergedLabel ? /* @__PURE__ */ jsx(Text2, { bold: true, color: MERGED_COLOUR, children: " " + mergedLabel }) : null
1506
1522
  ] });
1507
1523
  };
1508
1524
  var useActionMenu = () => {
@@ -1791,7 +1807,8 @@ var BrowseScreen = ({
1791
1807
  tabHelp,
1792
1808
  isWorkRepo,
1793
1809
  initialIncludeWork,
1794
- brand
1810
+ brand,
1811
+ mergedUrls
1795
1812
  }) => {
1796
1813
  const { rows } = useWindowSize();
1797
1814
  const [includeWork, setIncludeWork] = useState(
@@ -1881,6 +1898,13 @@ var BrowseScreen = ({
1881
1898
  setFlash(msg);
1882
1899
  setTimeout(() => setFlash(null), 2e3);
1883
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]);
1884
1908
  useEffect(() => {
1885
1909
  if (refreshError) showFlash(`\u2717 ${refreshError.message}`);
1886
1910
  }, [refreshError]);
@@ -2246,6 +2270,8 @@ var BrowseScreen = ({
2246
2270
  item,
2247
2271
  active: viewStart + i === cursor,
2248
2272
  login,
2273
+ merged: (item.kind === "pr" || item.kind === "issue") && !!mergedUrls?.includes(item.url),
2274
+ sparkFrame,
2249
2275
  gap: viewStart + i > 0 && (item.kind === "repo-header" || item.kind === "subgroup-header" || // A header is always followed by a blank line before its
2250
2276
  // first child — in Other PRs that's "free" because the
2251
2277
  // child is itself a repo-header (gap above). A jira row
@@ -2412,6 +2438,9 @@ var App = ({
2412
2438
  clearInterval(id);
2413
2439
  };
2414
2440
  }, [hasCiStatus, ciFetcher, ciPollMs]);
2441
+ const [mergedUrls, setMergedUrls] = useState([]);
2442
+ const mergeTimers = useRef([]);
2443
+ useEffect(() => () => mergeTimers.current.forEach(clearTimeout), []);
2415
2444
  if (state.phase === "loading" || state.phase === "empty" || state.phase === "failed")
2416
2445
  return /* @__PURE__ */ jsxs(
2417
2446
  Box,
@@ -2457,6 +2486,20 @@ var App = ({
2457
2486
  sections: withoutItem(state.sections, target),
2458
2487
  login: state.login
2459
2488
  });
2489
+ const markMerged = (target) => {
2490
+ toBrowse();
2491
+ setMergedUrls(
2492
+ (prev) => prev.includes(target.url) ? prev : [...prev, target.url]
2493
+ );
2494
+ mergeTimers.current.push(
2495
+ setTimeout(() => {
2496
+ setMergedUrls((prev) => prev.filter((u) => u !== target.url));
2497
+ setState(
2498
+ (s) => s.phase === "browse" ? { ...s, sections: withoutItem(s.sections, target) } : s
2499
+ );
2500
+ }, MERGED_HOLD_MS)
2501
+ );
2502
+ };
2460
2503
  return /* @__PURE__ */ jsxs(Fragment, { children: [
2461
2504
  /* @__PURE__ */ jsx(
2462
2505
  BrowseScreen,
@@ -2477,6 +2520,7 @@ var App = ({
2477
2520
  isWorkRepo,
2478
2521
  initialIncludeWork,
2479
2522
  hidden: overlay !== null,
2523
+ mergedUrls,
2480
2524
  ciStatusState: hasCiStatus ? ciStatusState : void 0,
2481
2525
  ciJob,
2482
2526
  tabHelp,
@@ -2508,7 +2552,8 @@ var App = ({
2508
2552
  login: state.login,
2509
2553
  onBack: toBrowse,
2510
2554
  onRefresh: applyOrRefresh,
2511
- onRemove: removeAndReturn
2555
+ onRemove: removeAndReturn,
2556
+ onMerged: markMerged
2512
2557
  }),
2513
2558
  overlay?.kind === "ext" && extensions?.find((e) => e.id === overlay.extId)?.body(toBrowse, overlay.target)
2514
2559
  ] });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kud/gh-ink",
3
- "version": "0.9.1",
3
+ "version": "0.11.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",