@kud/gh-ink 0.38.1 → 0.40.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
@@ -44,8 +44,25 @@ type SidebarRow = {
44
44
  * hard 0 asserts the first from evidence for neither.
45
45
  */
46
46
  live?: number;
47
+ /**
48
+ * How far through it is — `done` of `total` children resolved.
49
+ *
50
+ * Both or neither, and only ever drawn together: a numerator with no
51
+ * denominator is not progress, it is a number. Absent for the same reason
52
+ * `live` is — a host that cannot count says nothing rather than claiming zero,
53
+ * and the two counts genuinely can be unavailable while `live` is known, since
54
+ * `live` is what the board already drew and this needs asking Jira again.
55
+ */
56
+ done?: number;
57
+ total?: number;
47
58
  /** Something under it is waiting on you. */
48
59
  wantsYou?: boolean;
60
+ /**
61
+ * Where ↵ takes you. Absent means the row is a label and nothing more — the
62
+ * cursor still lands on it, and pressing ↵ does nothing rather than flashing
63
+ * an error about a host decision the reader cannot change.
64
+ */
65
+ url?: string;
49
66
  };
50
67
  type Sidebar = {
51
68
  /** Heading for the rail. The host's vocabulary, never ours. */
@@ -53,13 +70,23 @@ type Sidebar = {
53
70
  rows: SidebarRow[];
54
71
  };
55
72
  /**
56
- * The rail's width, INCLUDING its one-column gutter. Exported because the list
73
+ * The rail's width, INCLUDING its rule and padding. Exported because the list
57
74
  * beside it has to shrink by exactly this much: every row in that list truncates
58
75
  * against a budget, and a budget that does not know the rail is there overflows
59
76
  * by the rail's whole width — which in a frame sized to fill the terminal
60
77
  * scrolls the panel rather than clipping a row.
61
78
  */
62
- declare const SIDEBAR_COLS = 30;
79
+ declare const SIDEBAR_COLS = 40;
80
+ /**
81
+ * The dim figures after the key: how far through, and how much is moving.
82
+ *
83
+ * Either half may be missing and the other still worth saying, so this is a join
84
+ * of what is known rather than one of three fixed shapes. `done`/`total` are
85
+ * treated as a pair — a numerator with no denominator is not progress, it is a
86
+ * number — and a zero is printed wherever it was counted, because "nothing is
87
+ * moving" is exactly the thing a roadmap is read to notice.
88
+ */
89
+ declare const counts: (row: SidebarRow) => string;
63
90
  /**
64
91
  * How many rows fit in `height` — one fewer than would physically fit whenever
65
92
  * that means anything is left over, to buy the line that says so.
@@ -70,9 +97,26 @@ declare const SIDEBAR_COLS = 30;
70
97
  * initiative with nothing moving on it was invisible.
71
98
  */
72
99
  declare const railCapacity: (height: number, rows: number) => number;
73
- declare const SidePanel: ({ sidebar, height, }: {
100
+ /**
101
+ * A right-hand rail of initiatives, standing beside the list.
102
+ *
103
+ * Two lines per row rather than one, because both halves are load-bearing and
104
+ * neither survives the other being cut: a key with no words cannot be read at a
105
+ * glance, and words with no key cannot be opened. Forty columns is not enough for
106
+ * both on one line, so they stack.
107
+ *
108
+ * Presentational, like every other row renderer here: it DRAWS a cursor but does
109
+ * not own one, and it never calls `useInput`. Where the cursor is, and whether
110
+ * the arrows are pointed at this rail at all, are the host's state — which is
111
+ * what lets a screen with two focus regions have exactly one of them lit.
112
+ */
113
+ declare const SidePanel: ({ sidebar, height, focused, cursor, }: {
74
114
  sidebar: Sidebar;
75
115
  height?: number;
116
+ /** The arrows are pointed here, so this rail draws the cursor. */
117
+ focused?: boolean;
118
+ /** Which row the cursor is on. Only drawn while `focused`. */
119
+ cursor?: number;
76
120
  }) => React.JSX.Element;
77
121
 
78
122
  declare const renderMarkdown: (raw: string, width: number, fileLink?: (path: string, line?: number) => string) => StyledLine[];
@@ -675,4 +719,4 @@ declare const matchesFilter: (repo: string, filter: RepoFilter) => boolean;
675
719
  */
676
720
  declare const parsePatterns: (value: string) => string[];
677
721
 
678
- export { type Action, ActionMenu, type AnyItem, App, COLS, type CiStatus, CiStatusLine, type CiStatusState, CommentsPanel, type CommentsPanelProps, type DetailContext, type ExplainSection, type ExtensionTarget, type GHDetail, type GHItem, HealthPanel, type HealthPanelProps, type InboxBudget, type InboxConfig, type InboxExtension, type JiraTransition, type OriginSplit, PIN_MARK, type RepoFilter, type RepoHeader, type RepoProfile, SIDEBAR_COLS, type Section, type ShowLess, type ShowMore, SidePanel, type Sidebar, type SidebarRow, type Standing, type SubgroupHeader, type TaskRow, budgetNotice, buildActions, buildCheckoutCmd, checkoutDirs, clipboard, configureInbox, drillCmd, explainGhAction, explainItem, filterByOrigin, filterByRepos, filterBySearch, fitCount, gapsAbove, healthColor, healthDisplay, healthGlyph, healthLegend, inboxConfig, insertRepoHeaders, itermRun, jumpToRepo, jumpToRepoPane, layoutGHItems, matchesFilter, maxViewStart, moveCursor, openInTab, parsePatterns, profileOf, railCapacity, readCache, relativeTime, renderMarkdown, repoPriority, reposInSections, resetInboxConfig, resolveRepoPath, runHere, runInPane, runInPaneHorizontal, sameCiStatusState, signatureOf, sortByRecency, sortItems, toCiStatusState, topLevelCount, truncate, useActionMenu, whoseMove, windowCount, withHeaders, withoutItem, writeCache };
722
+ export { type Action, ActionMenu, type AnyItem, App, COLS, type CiStatus, CiStatusLine, type CiStatusState, CommentsPanel, type CommentsPanelProps, type DetailContext, type ExplainSection, type ExtensionTarget, type GHDetail, type GHItem, HealthPanel, type HealthPanelProps, type InboxBudget, type InboxConfig, type InboxExtension, type JiraTransition, type OriginSplit, PIN_MARK, type RepoFilter, type RepoHeader, type RepoProfile, SIDEBAR_COLS, type Section, type ShowLess, type ShowMore, SidePanel, type Sidebar, type SidebarRow, type Standing, type SubgroupHeader, type TaskRow, budgetNotice, buildActions, buildCheckoutCmd, checkoutDirs, clipboard, configureInbox, counts, drillCmd, explainGhAction, explainItem, filterByOrigin, filterByRepos, filterBySearch, fitCount, gapsAbove, healthColor, healthDisplay, healthGlyph, healthLegend, inboxConfig, insertRepoHeaders, itermRun, jumpToRepo, jumpToRepoPane, layoutGHItems, matchesFilter, maxViewStart, moveCursor, openInTab, parsePatterns, profileOf, railCapacity, readCache, relativeTime, renderMarkdown, repoPriority, reposInSections, resetInboxConfig, resolveRepoPath, runHere, runInPane, runInPaneHorizontal, sameCiStatusState, signatureOf, sortByRecency, sortItems, toCiStatusState, topLevelCount, truncate, useActionMenu, whoseMove, windowCount, withHeaders, withoutItem, writeCache };
package/dist/index.js CHANGED
@@ -584,9 +584,16 @@ var HealthPanel = ({
584
584
  ] });
585
585
  };
586
586
  var ACCENT = "#FF8700";
587
- var SIDEBAR_COLS = 30;
588
- var GUTTER = 1;
589
- var CONTENT = SIDEBAR_COLS - GUTTER;
587
+ var SIDEBAR_COLS = 40;
588
+ var RULE = 1;
589
+ var PAD = 2;
590
+ var CONTENT = SIDEBAR_COLS - RULE - PAD;
591
+ var LABEL_INDENT = 2;
592
+ var RULE_COLOR = "gray";
593
+ var counts = (row) => [
594
+ row.done !== void 0 && row.total !== void 0 ? `${row.done}/${row.total}` : "",
595
+ row.live !== void 0 ? `${row.live} live` : ""
596
+ ].filter(Boolean).join(" \xB7 ");
590
597
  var truncate = (text, max) => [...text].length <= max ? text : [...text].slice(0, Math.max(0, max - 1)).join("") + "\u2026";
591
598
  var ROW_LINES = 3;
592
599
  var HEADING_LINES = 2;
@@ -596,33 +603,56 @@ var railCapacity = (height, rows) => {
596
603
  };
597
604
  var SidePanel = ({
598
605
  sidebar,
599
- height
606
+ height,
607
+ focused = false,
608
+ cursor = 0
600
609
  }) => {
601
- const shown = height === void 0 ? sidebar.rows : sidebar.rows.slice(0, railCapacity(height, sidebar.rows.length));
610
+ const capacity = height === void 0 ? sidebar.rows.length : railCapacity(height, sidebar.rows.length);
611
+ const start = focused && cursor >= capacity ? Math.min(cursor - capacity + 1, sidebar.rows.length - capacity) : 0;
612
+ const shown = sidebar.rows.slice(start, start + capacity);
602
613
  const hidden = sidebar.rows.length - shown.length;
603
- return /* @__PURE__ */ jsxs(
604
- Box,
605
- {
606
- flexDirection: "column",
607
- width: SIDEBAR_COLS,
608
- flexShrink: 0,
609
- height,
610
- children: [
611
- /* @__PURE__ */ jsxs(Box, { marginBottom: 1, paddingLeft: GUTTER, children: [
612
- /* @__PURE__ */ jsx(Text, { color: ACCENT, bold: true, children: "\xBB " }),
613
- /* @__PURE__ */ jsx(Text, { bold: true, children: sidebar.title })
614
- ] }),
615
- sidebar.rows.length === 0 ? /* @__PURE__ */ jsx(Box, { paddingLeft: GUTTER + 2, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: "nothing open" }) }) : shown.map((row) => /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [
616
- /* @__PURE__ */ jsxs(Box, { paddingLeft: GUTTER, children: [
617
- /* @__PURE__ */ jsx(Text, { color: ACCENT, bold: true, children: row.wantsYou ? "\u2190 " : " " }),
618
- /* @__PURE__ */ jsx(Text, { color: ACCENT, children: row.key }),
619
- row.live !== void 0 ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: ` ${row.live} live` }) : null
614
+ return (
615
+ // A rule down the left rather than a full box: the rail's other three edges
616
+ // already have the frame's border a column or two away, and a second
617
+ // rectangle inside the first reads as a nested panel — something you could
618
+ // focus and act on, which this cannot be. One line is the whole claim: what
619
+ // is left of it is the list, what is right of it is not.
620
+ /* @__PURE__ */ jsxs(
621
+ Box,
622
+ {
623
+ flexDirection: "column",
624
+ width: SIDEBAR_COLS,
625
+ flexShrink: 0,
626
+ height,
627
+ borderStyle: "single",
628
+ borderColor: RULE_COLOR,
629
+ borderDimColor: true,
630
+ borderTop: false,
631
+ borderRight: false,
632
+ borderBottom: false,
633
+ paddingLeft: PAD,
634
+ children: [
635
+ /* @__PURE__ */ jsxs(Box, { marginBottom: 1, children: [
636
+ /* @__PURE__ */ jsx(Text, { color: ACCENT, bold: true, children: "\xBB " }),
637
+ /* @__PURE__ */ jsx(Text, { bold: true, children: sidebar.title }),
638
+ focused ? /* @__PURE__ */ jsx(Text, { color: ACCENT, children: " \u25CF focus" }) : null
620
639
  ] }),
621
- /* @__PURE__ */ jsx(Box, { paddingLeft: GUTTER + 2, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: truncate(row.label, CONTENT - 2) }) })
622
- ] }, row.key)),
623
- hidden > 0 ? /* @__PURE__ */ jsx(Box, { paddingLeft: GUTTER + 2, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: `+${hidden} more` }) }) : null
624
- ]
625
- }
640
+ sidebar.rows.length === 0 ? /* @__PURE__ */ jsx(Box, { paddingLeft: LABEL_INDENT, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: "nothing open" }) }) : shown.map((row, i) => {
641
+ const active = focused && start + i === cursor;
642
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [
643
+ /* @__PURE__ */ jsxs(Box, { children: [
644
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: active ? "\u276F " : " " }),
645
+ /* @__PURE__ */ jsx(Text, { color: ACCENT, bold: true, children: row.wantsYou ? "\u2190 " : " " }),
646
+ /* @__PURE__ */ jsx(Text, { color: ACCENT, bold: active, children: row.key }),
647
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: ` ${counts(row)}` })
648
+ ] }),
649
+ /* @__PURE__ */ jsx(Box, { paddingLeft: LABEL_INDENT + 2, children: /* @__PURE__ */ jsx(Text, { dimColor: !active, children: truncate(row.label, CONTENT - LABEL_INDENT - 2) }) })
650
+ ] }, row.key);
651
+ }),
652
+ hidden > 0 ? /* @__PURE__ */ jsx(Box, { paddingLeft: LABEL_INDENT, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: `+${hidden} more` }) }) : null
653
+ ]
654
+ }
655
+ )
626
656
  );
627
657
  };
628
658
  var healthDisplay = {
@@ -832,11 +862,11 @@ var diffSections = (prev, next) => {
832
862
  counts: { added, removed, changed: changedRows.size }
833
863
  };
834
864
  };
835
- var summariseDiff = (counts) => {
865
+ var summariseDiff = (counts2) => {
836
866
  const parts = [];
837
- if (counts.added) parts.push(`${counts.added} new`);
838
- if (counts.removed) parts.push(`${counts.removed} gone`);
839
- if (counts.changed) parts.push(`${counts.changed} moved`);
867
+ if (counts2.added) parts.push(`${counts2.added} new`);
868
+ if (counts2.removed) parts.push(`${counts2.removed} gone`);
869
+ if (counts2.changed) parts.push(`${counts2.changed} moved`);
840
870
  return parts.join(" \xB7 ");
841
871
  };
842
872
  var transientOf = (transients, item, sectionId) => {
@@ -2478,8 +2508,13 @@ var BrowseScreen = ({
2478
2508
  out.add(s.id);
2479
2509
  return out;
2480
2510
  }, [localSections, transients]);
2481
- const [railOpen, setRailOpen] = useState(false);
2511
+ const [railOpen, setRailOpen] = useState(true);
2482
2512
  const showRail = railOpen && !!sidebar;
2513
+ const [railFocus, setRailFocus] = useState(false);
2514
+ const [railCursor, setRailCursor] = useState(0);
2515
+ const railRows = sidebar?.rows ?? [];
2516
+ const railActive = showRail && railFocus && railRows.length > 0;
2517
+ const railAt = Math.min(railCursor, Math.max(0, railRows.length - 1));
2483
2518
  const listCols = showRail ? COLS - SIDEBAR_COLS : COLS;
2484
2519
  const [sparkFrame, setSparkFrame] = useState(0);
2485
2520
  const sparkling = (mergedUrls?.length ?? 0) > 0 || (leavingUrls?.length ?? 0) > 0 || !!transients?.size && section.items.some((item) => transientOf(transients, item, section.id));
@@ -2582,9 +2617,41 @@ var BrowseScreen = ({
2582
2617
  return;
2583
2618
  }
2584
2619
  if (input === "i" && sidebar) {
2585
- setRailOpen((open) => !open);
2620
+ setRailOpen((open) => {
2621
+ if (open) setRailFocus(false);
2622
+ return !open;
2623
+ });
2586
2624
  return;
2587
2625
  }
2626
+ if (key.tab && showRail) {
2627
+ setRailFocus((f) => !f);
2628
+ return;
2629
+ }
2630
+ if (railActive) {
2631
+ if (key.upArrow || input === "k") {
2632
+ setRailCursor((c) => Math.max(0, c - 1));
2633
+ return;
2634
+ }
2635
+ if (key.downArrow || input === "j") {
2636
+ setRailCursor((c) => Math.min(railRows.length - 1, c + 1));
2637
+ return;
2638
+ }
2639
+ if (key.escape) {
2640
+ setRailFocus(false);
2641
+ return;
2642
+ }
2643
+ if (key.return || input === "o") {
2644
+ const row = railRows[railAt];
2645
+ if (row?.url) {
2646
+ quietly`open ${row.url}`.catch(() => {
2647
+ });
2648
+ showFlash(`\u2197 Opened ${row.key}`);
2649
+ }
2650
+ return;
2651
+ }
2652
+ if (input !== "r" && input !== "q" && !key.leftArrow && !key.rightArrow)
2653
+ return;
2654
+ }
2588
2655
  if (key.escape && search != null) {
2589
2656
  setSearch(null);
2590
2657
  return;
@@ -2857,7 +2924,19 @@ var BrowseScreen = ({
2857
2924
  menu.open(actions);
2858
2925
  }
2859
2926
  });
2860
- const hints = activeItem?.kind === "repo-header" ? [
2927
+ const hints = railActive ? (
2928
+ // The rail's own keymap while it holds focus, not the list's with a line
2929
+ // bolted on. A footer advertising `m actions` beside a cursor that cannot
2930
+ // reach a row is worse than a short footer: it names a key that does
2931
+ // nothing where you are standing.
2932
+ [
2933
+ ["\u2191\u2193", "initiative"],
2934
+ ["\u21B5", "open"],
2935
+ ["\u21E5/esc", "back to list"],
2936
+ ["?", "help"],
2937
+ ["q", "quit"]
2938
+ ]
2939
+ ) : activeItem?.kind === "repo-header" ? [
2861
2940
  ["\u2191\u2193", "nav"],
2862
2941
  ["\u2190\u2192", "tab"],
2863
2942
  ["\u21B5", "open repo"],
@@ -2872,7 +2951,7 @@ var BrowseScreen = ({
2872
2951
  ["m", "actions"],
2873
2952
  // Advertised only where it does something, and named for what it
2874
2953
  // shows rather than for the furniture: nobody wants "a sidebar".
2875
- ...sidebar ? [["i", railOpen ? "hide" : sidebar.title.toLowerCase()]] : [],
2954
+ ...showRail ? [["\u21E5", sidebar.title.toLowerCase()]] : sidebar ? [["i", sidebar.title.toLowerCase()]] : [],
2876
2955
  ["?", "help"],
2877
2956
  ["q", "quit"]
2878
2957
  ];
@@ -2996,7 +3075,15 @@ var BrowseScreen = ({
2996
3075
  ]
2997
3076
  }
2998
3077
  ),
2999
- showRail && sidebar ? /* @__PURE__ */ jsx(SidePanel, { sidebar, height: listHeight }) : null
3078
+ showRail && sidebar ? /* @__PURE__ */ jsx(
3079
+ SidePanel,
3080
+ {
3081
+ sidebar,
3082
+ height: listHeight,
3083
+ focused: railActive,
3084
+ cursor: railAt
3085
+ }
3086
+ ) : null
3000
3087
  ] }),
3001
3088
  /* @__PURE__ */ jsx(Box, { marginTop: 1, children: flash ? /* @__PURE__ */ jsx(Text3, { color: "green", children: flash }) : /* @__PURE__ */ jsx(FooterHints, { hints }) })
3002
3089
  ]
@@ -3205,8 +3292,8 @@ var App = ({
3205
3292
  }
3206
3293
  showData(fresh.sections, fresh.login);
3207
3294
  } else if (freshKey !== displayedKey.current) {
3208
- const { counts } = diffSections(displayedSections.current, fresh.sections);
3209
- if (counts.added + counts.removed + counts.changed === 0)
3295
+ const { counts: counts2 } = diffSections(displayedSections.current, fresh.sections);
3296
+ if (counts2.added + counts2.removed + counts2.changed === 0)
3210
3297
  showData(fresh.sections, fresh.login);
3211
3298
  else setPending(fresh);
3212
3299
  } else {
@@ -3450,4 +3537,4 @@ var matchesFilter = (repo, filter) => {
3450
3537
  };
3451
3538
  var parsePatterns = (value) => value.split(",").map((p) => p.trim()).filter(Boolean);
3452
3539
 
3453
- export { ActionMenu, App, COLS, CiStatusLine, CommentsPanel, HealthPanel, PIN_MARK, SIDEBAR_COLS, SidePanel, budgetNotice, buildActions, buildCheckoutCmd, checkoutDirs, clipboard, configureInbox, drillCmd, explainGhAction, explainItem, filterByOrigin, filterByRepos, filterBySearch, fitCount, gapsAbove, healthColor, healthDisplay, healthGlyph, healthLegend, inboxConfig, insertRepoHeaders, itermRun, jumpToRepo, jumpToRepoPane, layoutGHItems, matchesFilter, maxViewStart, moveCursor, openInTab, parsePatterns, profileOf, railCapacity, readCache, relativeTime, renderMarkdown, repoPriority, reposInSections, resetInboxConfig, resolveRepoPath, runHere, runInPane, runInPaneHorizontal, sameCiStatusState, signatureOf, sortByRecency, sortItems, toCiStatusState, topLevelCount, truncate2 as truncate, useActionMenu, whoseMove, windowCount, withHeaders, withoutItem, writeCache };
3540
+ export { ActionMenu, App, COLS, CiStatusLine, CommentsPanel, HealthPanel, PIN_MARK, SIDEBAR_COLS, SidePanel, budgetNotice, buildActions, buildCheckoutCmd, checkoutDirs, clipboard, configureInbox, counts, drillCmd, explainGhAction, explainItem, filterByOrigin, filterByRepos, filterBySearch, fitCount, gapsAbove, healthColor, healthDisplay, healthGlyph, healthLegend, inboxConfig, insertRepoHeaders, itermRun, jumpToRepo, jumpToRepoPane, layoutGHItems, matchesFilter, maxViewStart, moveCursor, openInTab, parsePatterns, profileOf, railCapacity, readCache, relativeTime, renderMarkdown, repoPriority, reposInSections, resetInboxConfig, resolveRepoPath, runHere, runInPane, runInPaneHorizontal, sameCiStatusState, signatureOf, sortByRecency, sortItems, toCiStatusState, topLevelCount, truncate2 as truncate, useActionMenu, whoseMove, windowCount, withHeaders, withoutItem, writeCache };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kud/gh-ink",
3
- "version": "0.38.1",
3
+ "version": "0.40.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",