@kud/gh-ink 0.38.1 → 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,13 +59,13 @@ 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;
63
69
  /**
64
70
  * How many rows fit in `height` — one fewer than would physically fit whenever
65
71
  * that means anything is left over, to buy the line that says so.
@@ -70,9 +76,26 @@ declare const SIDEBAR_COLS = 30;
70
76
  * initiative with nothing moving on it was invisible.
71
77
  */
72
78
  declare const railCapacity: (height: number, rows: number) => number;
73
- declare const SidePanel: ({ sidebar, height, }: {
79
+ /**
80
+ * A right-hand rail of initiatives, standing beside the list.
81
+ *
82
+ * Two lines per row rather than one, because both halves are load-bearing and
83
+ * neither survives the other being cut: a key with no words cannot be read at a
84
+ * glance, and words with no key cannot be opened. Forty columns is not enough for
85
+ * both on one line, so they stack.
86
+ *
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.
91
+ */
92
+ declare const SidePanel: ({ sidebar, height, focused, cursor, }: {
74
93
  sidebar: Sidebar;
75
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;
76
99
  }) => React.JSX.Element;
77
100
 
78
101
  declare const renderMarkdown: (raw: string, width: number, fileLink?: (path: string, line?: number) => string) => StyledLine[];
package/dist/index.js CHANGED
@@ -584,9 +584,12 @@ 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";
591
594
  var ROW_LINES = 3;
592
595
  var HEADING_LINES = 2;
@@ -596,33 +599,56 @@ var railCapacity = (height, rows) => {
596
599
  };
597
600
  var SidePanel = ({
598
601
  sidebar,
599
- height
602
+ height,
603
+ focused = false,
604
+ cursor = 0
600
605
  }) => {
601
- const shown = height === void 0 ? sidebar.rows : sidebar.rows.slice(0, railCapacity(height, sidebar.rows.length));
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);
602
609
  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
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
620
635
  ] }),
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
- }
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
+ )
626
652
  );
627
653
  };
628
654
  var healthDisplay = {
@@ -2478,8 +2504,13 @@ var BrowseScreen = ({
2478
2504
  out.add(s.id);
2479
2505
  return out;
2480
2506
  }, [localSections, transients]);
2481
- const [railOpen, setRailOpen] = useState(false);
2507
+ const [railOpen, setRailOpen] = useState(true);
2482
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));
2483
2514
  const listCols = showRail ? COLS - SIDEBAR_COLS : COLS;
2484
2515
  const [sparkFrame, setSparkFrame] = useState(0);
2485
2516
  const sparkling = (mergedUrls?.length ?? 0) > 0 || (leavingUrls?.length ?? 0) > 0 || !!transients?.size && section.items.some((item) => transientOf(transients, item, section.id));
@@ -2582,9 +2613,41 @@ var BrowseScreen = ({
2582
2613
  return;
2583
2614
  }
2584
2615
  if (input === "i" && sidebar) {
2585
- setRailOpen((open) => !open);
2616
+ setRailOpen((open) => {
2617
+ if (open) setRailFocus(false);
2618
+ return !open;
2619
+ });
2586
2620
  return;
2587
2621
  }
2622
+ if (key.tab && showRail) {
2623
+ setRailFocus((f) => !f);
2624
+ return;
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
+ }
2588
2651
  if (key.escape && search != null) {
2589
2652
  setSearch(null);
2590
2653
  return;
@@ -2857,7 +2920,19 @@ var BrowseScreen = ({
2857
2920
  menu.open(actions);
2858
2921
  }
2859
2922
  });
2860
- 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" ? [
2861
2936
  ["\u2191\u2193", "nav"],
2862
2937
  ["\u2190\u2192", "tab"],
2863
2938
  ["\u21B5", "open repo"],
@@ -2872,7 +2947,7 @@ var BrowseScreen = ({
2872
2947
  ["m", "actions"],
2873
2948
  // Advertised only where it does something, and named for what it
2874
2949
  // shows rather than for the furniture: nobody wants "a sidebar".
2875
- ...sidebar ? [["i", railOpen ? "hide" : sidebar.title.toLowerCase()]] : [],
2950
+ ...showRail ? [["\u21E5", sidebar.title.toLowerCase()]] : sidebar ? [["i", sidebar.title.toLowerCase()]] : [],
2876
2951
  ["?", "help"],
2877
2952
  ["q", "quit"]
2878
2953
  ];
@@ -2996,7 +3071,15 @@ var BrowseScreen = ({
2996
3071
  ]
2997
3072
  }
2998
3073
  ),
2999
- 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
3000
3083
  ] }),
3001
3084
  /* @__PURE__ */ jsx(Box, { marginTop: 1, children: flash ? /* @__PURE__ */ jsx(Text3, { color: "green", children: flash }) : /* @__PURE__ */ jsx(FooterHints, { hints }) })
3002
3085
  ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kud/gh-ink",
3
- "version": "0.38.1",
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",