@kud/gh-ink 0.38.0 → 0.39.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
@@ -46,6 +46,12 @@ type SidebarRow = {
46
46
  live?: number;
47
47
  /** Something under it is waiting on you. */
48
48
  wantsYou?: boolean;
49
+ /**
50
+ * Where ↵ takes you. Absent means the row is a label and nothing more — the
51
+ * cursor still lands on it, and pressing ↵ does nothing rather than flashing
52
+ * an error about a host decision the reader cannot change.
53
+ */
54
+ url?: string;
49
55
  };
50
56
  type Sidebar = {
51
57
  /** Heading for the rail. The host's vocabulary, never ours. */
@@ -53,27 +59,43 @@ type Sidebar = {
53
59
  rows: SidebarRow[];
54
60
  };
55
61
  /**
56
- * The rail's width, INCLUDING its one-column gutter. Exported because the list
62
+ * The rail's width, INCLUDING its rule and padding. Exported because the list
57
63
  * beside it has to shrink by exactly this much: every row in that list truncates
58
64
  * against a budget, and a budget that does not know the rail is there overflows
59
65
  * by the rail's whole width — which in a frame sized to fill the terminal
60
66
  * scrolls the panel rather than clipping a row.
61
67
  */
62
- declare const SIDEBAR_COLS = 30;
68
+ declare const SIDEBAR_COLS = 40;
69
+ /**
70
+ * How many rows fit in `height` — one fewer than would physically fit whenever
71
+ * that means anything is left over, to buy the line that says so.
72
+ *
73
+ * Computed rather than left to the layout, because the layout's answer is to CUT,
74
+ * silently. A roadmap quietly missing its last three initiatives looks exactly
75
+ * like a roadmap that has none, and the whole reason the rail exists is that an
76
+ * initiative with nothing moving on it was invisible.
77
+ */
78
+ declare const railCapacity: (height: number, rows: number) => number;
63
79
  /**
64
80
  * A right-hand rail of initiatives, standing beside the list.
65
81
  *
66
82
  * Two lines per row rather than one, because both halves are load-bearing and
67
83
  * neither survives the other being cut: a key with no words cannot be read at a
68
- * glance, and words with no key cannot be opened. Thirty columns is not enough
69
- * for both on one line, so they stack.
84
+ * glance, and words with no key cannot be opened. Forty columns is not enough for
85
+ * both on one line, so they stack.
70
86
  *
71
- * Presentational, like every other row renderer here — it takes no keyboard and
72
- * holds no cursor. Whether the rail is shown at all is the host's state.
87
+ * Presentational, like every other row renderer here: it DRAWS a cursor but does
88
+ * not own one, and it never calls `useInput`. Where the cursor is, and whether
89
+ * the arrows are pointed at this rail at all, are the host's state — which is
90
+ * what lets a screen with two focus regions have exactly one of them lit.
73
91
  */
74
- declare const SidePanel: ({ sidebar, height, }: {
92
+ declare const SidePanel: ({ sidebar, height, focused, cursor, }: {
75
93
  sidebar: Sidebar;
76
94
  height?: number;
95
+ /** The arrows are pointed here, so this rail draws the cursor. */
96
+ focused?: boolean;
97
+ /** Which row the cursor is on. Only drawn while `focused`. */
98
+ cursor?: number;
77
99
  }) => React.JSX.Element;
78
100
 
79
101
  declare const renderMarkdown: (raw: string, width: number, fileLink?: (path: string, line?: number) => string) => StyledLine[];
@@ -676,4 +698,4 @@ declare const matchesFilter: (repo: string, filter: RepoFilter) => boolean;
676
698
  */
677
699
  declare const parsePatterns: (value: string) => string[];
678
700
 
679
- 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, readCache, relativeTime, renderMarkdown, repoPriority, reposInSections, resetInboxConfig, resolveRepoPath, runHere, runInPane, runInPaneHorizontal, sameCiStatusState, signatureOf, sortByRecency, sortItems, toCiStatusState, topLevelCount, truncate, useActionMenu, whoseMove, windowCount, withHeaders, withoutItem, writeCache };
701
+ 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 };
package/dist/index.js CHANGED
@@ -584,36 +584,73 @@ 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";
590
593
  var truncate = (text, max) => [...text].length <= max ? text : [...text].slice(0, Math.max(0, max - 1)).join("") + "\u2026";
594
+ var ROW_LINES = 3;
595
+ var HEADING_LINES = 2;
596
+ var railCapacity = (height, rows) => {
597
+ const fits = Math.max(0, Math.floor((height - HEADING_LINES) / ROW_LINES));
598
+ return fits >= rows ? rows : Math.max(0, fits - 1);
599
+ };
591
600
  var SidePanel = ({
592
601
  sidebar,
593
- height
594
- }) => /* @__PURE__ */ jsxs(
595
- Box,
596
- {
597
- flexDirection: "column",
598
- width: SIDEBAR_COLS,
599
- flexShrink: 0,
600
- height,
601
- children: [
602
- /* @__PURE__ */ jsxs(Box, { marginBottom: 1, paddingLeft: GUTTER, children: [
603
- /* @__PURE__ */ jsx(Text, { color: ACCENT, bold: true, children: "\xBB " }),
604
- /* @__PURE__ */ jsx(Text, { bold: true, children: sidebar.title })
605
- ] }),
606
- sidebar.rows.length === 0 ? /* @__PURE__ */ jsx(Box, { paddingLeft: GUTTER + 2, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: "nothing open" }) }) : sidebar.rows.map((row) => /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [
607
- /* @__PURE__ */ jsxs(Box, { paddingLeft: GUTTER, children: [
608
- /* @__PURE__ */ jsx(Text, { color: ACCENT, bold: true, children: row.wantsYou ? "\u2190 " : " " }),
609
- /* @__PURE__ */ jsx(Text, { color: ACCENT, children: row.key }),
610
- row.live !== void 0 ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: ` ${row.live} live` }) : null
611
- ] }),
612
- /* @__PURE__ */ jsx(Box, { paddingLeft: GUTTER + 2, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: truncate(row.label, CONTENT - 2) }) })
613
- ] }, row.key))
614
- ]
615
- }
616
- );
602
+ height,
603
+ focused = false,
604
+ cursor = 0
605
+ }) => {
606
+ const capacity = height === void 0 ? sidebar.rows.length : railCapacity(height, sidebar.rows.length);
607
+ const start = focused && cursor >= capacity ? Math.min(cursor - capacity + 1, sidebar.rows.length - capacity) : 0;
608
+ const shown = sidebar.rows.slice(start, start + capacity);
609
+ const hidden = sidebar.rows.length - shown.length;
610
+ return (
611
+ // A rule down the left rather than a full box: the rail's other three edges
612
+ // already have the frame's border a column or two away, and a second
613
+ // rectangle inside the first reads as a nested panel — something you could
614
+ // focus and act on, which this cannot be. One line is the whole claim: what
615
+ // is left of it is the list, what is right of it is not.
616
+ /* @__PURE__ */ jsxs(
617
+ Box,
618
+ {
619
+ flexDirection: "column",
620
+ width: SIDEBAR_COLS,
621
+ flexShrink: 0,
622
+ height,
623
+ borderStyle: "single",
624
+ borderColor: RULE_COLOR,
625
+ borderDimColor: true,
626
+ borderTop: false,
627
+ borderRight: false,
628
+ borderBottom: false,
629
+ paddingLeft: PAD,
630
+ children: [
631
+ /* @__PURE__ */ jsxs(Box, { marginBottom: 1, children: [
632
+ /* @__PURE__ */ jsx(Text, { color: ACCENT, bold: true, children: "\xBB " }),
633
+ /* @__PURE__ */ jsx(Text, { bold: true, children: sidebar.title }),
634
+ focused ? /* @__PURE__ */ jsx(Text, { color: ACCENT, children: " \u25CF focus" }) : null
635
+ ] }),
636
+ sidebar.rows.length === 0 ? /* @__PURE__ */ jsx(Box, { paddingLeft: LABEL_INDENT, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: "nothing open" }) }) : shown.map((row, i) => {
637
+ const active = focused && start + i === cursor;
638
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [
639
+ /* @__PURE__ */ jsxs(Box, { children: [
640
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: active ? "\u276F " : " " }),
641
+ /* @__PURE__ */ jsx(Text, { color: ACCENT, bold: true, children: row.wantsYou ? "\u2190 " : " " }),
642
+ /* @__PURE__ */ jsx(Text, { color: ACCENT, bold: active, children: row.key }),
643
+ row.live !== void 0 ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: ` ${row.live} live` }) : null
644
+ ] }),
645
+ /* @__PURE__ */ jsx(Box, { paddingLeft: LABEL_INDENT + 2, children: /* @__PURE__ */ jsx(Text, { dimColor: !active, children: truncate(row.label, CONTENT - LABEL_INDENT - 2) }) })
646
+ ] }, row.key);
647
+ }),
648
+ hidden > 0 ? /* @__PURE__ */ jsx(Box, { paddingLeft: LABEL_INDENT, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: `+${hidden} more` }) }) : null
649
+ ]
650
+ }
651
+ )
652
+ );
653
+ };
617
654
  var healthDisplay = {
618
655
  none: { glyph: " ", color: colors.muted },
619
656
  draft: { glyph: "~", color: colors.muted },
@@ -2467,8 +2504,13 @@ var BrowseScreen = ({
2467
2504
  out.add(s.id);
2468
2505
  return out;
2469
2506
  }, [localSections, transients]);
2470
- const [railOpen, setRailOpen] = useState(false);
2507
+ const [railOpen, setRailOpen] = useState(true);
2471
2508
  const showRail = railOpen && !!sidebar;
2509
+ const [railFocus, setRailFocus] = useState(false);
2510
+ const [railCursor, setRailCursor] = useState(0);
2511
+ const railRows = sidebar?.rows ?? [];
2512
+ const railActive = showRail && railFocus && railRows.length > 0;
2513
+ const railAt = Math.min(railCursor, Math.max(0, railRows.length - 1));
2472
2514
  const listCols = showRail ? COLS - SIDEBAR_COLS : COLS;
2473
2515
  const [sparkFrame, setSparkFrame] = useState(0);
2474
2516
  const sparkling = (mergedUrls?.length ?? 0) > 0 || (leavingUrls?.length ?? 0) > 0 || !!transients?.size && section.items.some((item) => transientOf(transients, item, section.id));
@@ -2571,9 +2613,41 @@ var BrowseScreen = ({
2571
2613
  return;
2572
2614
  }
2573
2615
  if (input === "i" && sidebar) {
2574
- setRailOpen((open) => !open);
2616
+ setRailOpen((open) => {
2617
+ if (open) setRailFocus(false);
2618
+ return !open;
2619
+ });
2620
+ return;
2621
+ }
2622
+ if (key.tab && showRail) {
2623
+ setRailFocus((f) => !f);
2575
2624
  return;
2576
2625
  }
2626
+ if (railActive) {
2627
+ if (key.upArrow || input === "k") {
2628
+ setRailCursor((c) => Math.max(0, c - 1));
2629
+ return;
2630
+ }
2631
+ if (key.downArrow || input === "j") {
2632
+ setRailCursor((c) => Math.min(railRows.length - 1, c + 1));
2633
+ return;
2634
+ }
2635
+ if (key.escape) {
2636
+ setRailFocus(false);
2637
+ return;
2638
+ }
2639
+ if (key.return || input === "o") {
2640
+ const row = railRows[railAt];
2641
+ if (row?.url) {
2642
+ quietly`open ${row.url}`.catch(() => {
2643
+ });
2644
+ showFlash(`\u2197 Opened ${row.key}`);
2645
+ }
2646
+ return;
2647
+ }
2648
+ if (input !== "r" && input !== "q" && !key.leftArrow && !key.rightArrow)
2649
+ return;
2650
+ }
2577
2651
  if (key.escape && search != null) {
2578
2652
  setSearch(null);
2579
2653
  return;
@@ -2846,7 +2920,19 @@ var BrowseScreen = ({
2846
2920
  menu.open(actions);
2847
2921
  }
2848
2922
  });
2849
- const hints = activeItem?.kind === "repo-header" ? [
2923
+ const hints = railActive ? (
2924
+ // The rail's own keymap while it holds focus, not the list's with a line
2925
+ // bolted on. A footer advertising `m actions` beside a cursor that cannot
2926
+ // reach a row is worse than a short footer: it names a key that does
2927
+ // nothing where you are standing.
2928
+ [
2929
+ ["\u2191\u2193", "initiative"],
2930
+ ["\u21B5", "open"],
2931
+ ["\u21E5/esc", "back to list"],
2932
+ ["?", "help"],
2933
+ ["q", "quit"]
2934
+ ]
2935
+ ) : activeItem?.kind === "repo-header" ? [
2850
2936
  ["\u2191\u2193", "nav"],
2851
2937
  ["\u2190\u2192", "tab"],
2852
2938
  ["\u21B5", "open repo"],
@@ -2861,7 +2947,7 @@ var BrowseScreen = ({
2861
2947
  ["m", "actions"],
2862
2948
  // Advertised only where it does something, and named for what it
2863
2949
  // shows rather than for the furniture: nobody wants "a sidebar".
2864
- ...sidebar ? [["i", railOpen ? "hide" : sidebar.title.toLowerCase()]] : [],
2950
+ ...showRail ? [["\u21E5", sidebar.title.toLowerCase()]] : sidebar ? [["i", sidebar.title.toLowerCase()]] : [],
2865
2951
  ["?", "help"],
2866
2952
  ["q", "quit"]
2867
2953
  ];
@@ -2985,7 +3071,15 @@ var BrowseScreen = ({
2985
3071
  ]
2986
3072
  }
2987
3073
  ),
2988
- showRail && sidebar ? /* @__PURE__ */ jsx(SidePanel, { sidebar, height: listHeight }) : null
3074
+ showRail && sidebar ? /* @__PURE__ */ jsx(
3075
+ SidePanel,
3076
+ {
3077
+ sidebar,
3078
+ height: listHeight,
3079
+ focused: railActive,
3080
+ cursor: railAt
3081
+ }
3082
+ ) : null
2989
3083
  ] }),
2990
3084
  /* @__PURE__ */ jsx(Box, { marginTop: 1, children: flash ? /* @__PURE__ */ jsx(Text3, { color: "green", children: flash }) : /* @__PURE__ */ jsx(FooterHints, { hints }) })
2991
3085
  ]
@@ -3439,4 +3533,4 @@ var matchesFilter = (repo, filter) => {
3439
3533
  };
3440
3534
  var parsePatterns = (value) => value.split(",").map((p) => p.trim()).filter(Boolean);
3441
3535
 
3442
- 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, 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 };
3536
+ 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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kud/gh-ink",
3
- "version": "0.38.0",
3
+ "version": "0.39.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",