@ondrej-svec/hog 1.12.0 → 1.14.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/cli.js CHANGED
@@ -2014,7 +2014,8 @@ function useKeyboard({
2014
2014
  activityNav,
2015
2015
  onRepoEnter,
2016
2016
  onStatusEnter,
2017
- onActivityEnter
2017
+ onActivityEnter,
2018
+ showDetailPanel
2018
2019
  }) {
2019
2020
  const {
2020
2021
  exit,
@@ -2115,7 +2116,11 @@ function useKeyboard({
2115
2116
  if (ui.canAct) {
2116
2117
  const digit = parseInt(input2, 10);
2117
2118
  if (!Number.isNaN(digit) && digit >= 0 && digit <= 4) {
2118
- panelFocus.focusPanel(digit);
2119
+ if (digit === 0 && !showDetailPanel) {
2120
+ ui.enterDetail();
2121
+ } else {
2122
+ panelFocus.focusPanel(digit);
2123
+ }
2119
2124
  return;
2120
2125
  }
2121
2126
  if (input2 === "/") {
@@ -2721,6 +2726,9 @@ function uiReducer(state, action) {
2721
2726
  case "ENTER_EDIT_ISSUE":
2722
2727
  if (state.mode !== "normal") return state;
2723
2728
  return { ...state, mode: "overlay:editIssue", previousMode: "normal" };
2729
+ case "ENTER_DETAIL":
2730
+ if (state.mode !== "normal") return state;
2731
+ return { ...state, mode: "overlay:detail", previousMode: "normal" };
2724
2732
  case "TOGGLE_HELP":
2725
2733
  return { ...state, helpVisible: !state.helpVisible };
2726
2734
  case "EXIT_OVERLAY":
@@ -2765,6 +2773,7 @@ function useUIState() {
2765
2773
  enterFocus: useCallback9(() => dispatch({ type: "ENTER_FOCUS" }), []),
2766
2774
  enterFuzzyPicker: useCallback9(() => dispatch({ type: "ENTER_FUZZY_PICKER" }), []),
2767
2775
  enterEditIssue: useCallback9(() => dispatch({ type: "ENTER_EDIT_ISSUE" }), []),
2776
+ enterDetail: useCallback9(() => dispatch({ type: "ENTER_DETAIL" }), []),
2768
2777
  toggleHelp: useCallback9(() => dispatch({ type: "TOGGLE_HELP" }), []),
2769
2778
  exitOverlay: useCallback9(() => dispatch({ type: "EXIT_OVERLAY" }), []),
2770
2779
  exitToNormal: useCallback9(() => dispatch({ type: "EXIT_TO_NORMAL" }), []),
@@ -3095,6 +3104,12 @@ function HintBar({
3095
3104
  if (uiMode === "overlay:fuzzyPicker") {
3096
3105
  return /* @__PURE__ */ jsx5(Box5, { children: /* @__PURE__ */ jsx5(Text5, { color: "gray", children: "\u2191\u2193/Ctrl-J/K:nav Enter:jump Esc:close" }) });
3097
3106
  }
3107
+ if (uiMode === "overlay:detail") {
3108
+ return /* @__PURE__ */ jsxs5(Box5, { children: [
3109
+ /* @__PURE__ */ jsx5(Text5, { color: "cyan", bold: true, children: "[DETAIL]" }),
3110
+ /* @__PURE__ */ jsx5(Text5, { color: "gray", children: " Esc:close e:edit c:comment y:copy-link ? help" })
3111
+ ] });
3112
+ }
3098
3113
  if (uiMode.startsWith("overlay:")) {
3099
3114
  return /* @__PURE__ */ jsx5(Box5, { children: /* @__PURE__ */ jsx5(Text5, { color: "gray", children: "j/k:nav Enter:select Esc:cancel" }) });
3100
3115
  }
@@ -4162,6 +4177,7 @@ var init_help_overlay = __esm({
4162
4177
  category: "View",
4163
4178
  items: [
4164
4179
  { key: "Enter", desc: "Open issue in browser" },
4180
+ { key: "0", desc: "Detail panel (full-screen on narrow terminals)" },
4165
4181
  { key: "Space", desc: "Multi-select item" },
4166
4182
  { key: "/", desc: "Search (inline filter)" },
4167
4183
  { key: "F", desc: "Fuzzy find issue (telescope-style)" },
@@ -4845,47 +4861,69 @@ var init_repos_panel = __esm({
4845
4861
 
4846
4862
  // src/board/components/issue-row.tsx
4847
4863
  import { Box as Box20, Text as Text19 } from "ink";
4848
- import { Fragment as Fragment3, jsx as jsx21, jsxs as jsxs21 } from "react/jsx-runtime";
4864
+ import { jsx as jsx21, jsxs as jsxs21 } from "react/jsx-runtime";
4849
4865
  function truncate(s, max) {
4850
4866
  return s.length > max ? `${s.slice(0, max - 1)}\u2026` : s;
4851
4867
  }
4852
- function formatTargetDate(dateStr) {
4853
- if (!dateStr) return { text: "", color: "gray" };
4854
- const d = new Date(dateStr);
4855
- const days = Math.ceil((d.getTime() - Date.now()) / 864e5);
4856
- if (days < 0) return { text: `${Math.abs(days)}d overdue`, color: "red" };
4857
- if (days === 0) return { text: "today", color: "yellow" };
4858
- if (days === 1) return { text: "tomorrow", color: "white" };
4859
- if (days <= 7) return { text: `in ${days}d`, color: "white" };
4860
- return {
4861
- text: d.toLocaleDateString("en-US", { month: "short", day: "numeric" }),
4862
- color: "gray"
4863
- };
4864
- }
4865
- function timeAgo2(dateStr) {
4866
- const seconds = Math.floor((Date.now() - new Date(dateStr).getTime()) / 1e3);
4867
- if (seconds < 60) return "now";
4868
+ function formatDate2(issue) {
4869
+ if (issue.targetDate) {
4870
+ const d = new Date(issue.targetDate);
4871
+ const days2 = Math.ceil((d.getTime() - Date.now()) / 864e5);
4872
+ if (days2 < 0) return { text: `${Math.abs(days2)}d overdue`, color: "red" };
4873
+ if (days2 === 0) return { text: "today", color: "yellow" };
4874
+ if (days2 === 1) return { text: "tomorrow", color: "white" };
4875
+ if (days2 <= 7) return { text: `in ${days2}d`, color: "white" };
4876
+ return {
4877
+ text: d.toLocaleDateString("en-US", { month: "short", day: "numeric" }),
4878
+ color: "gray"
4879
+ };
4880
+ }
4881
+ const seconds = Math.floor((Date.now() - new Date(issue.updatedAt).getTime()) / 1e3);
4882
+ if (seconds < 60) return { text: "now", color: "gray" };
4868
4883
  const minutes = Math.floor(seconds / 60);
4869
- if (minutes < 60) return `${minutes}m`;
4884
+ if (minutes < 60) return { text: `${minutes}m`, color: "gray" };
4870
4885
  const hours = Math.floor(minutes / 60);
4871
- if (hours < 24) return `${hours}h`;
4886
+ if (hours < 24) return { text: `${hours}h`, color: "gray" };
4872
4887
  const days = Math.floor(hours / 24);
4873
- if (days < 30) return `${days}d`;
4888
+ if (days < 30) return { text: `${days}d`, color: "gray" };
4874
4889
  const months = Math.floor(days / 30);
4875
- return `${months}mo`;
4890
+ return { text: `${months}mo`, color: "gray" };
4891
+ }
4892
+ function compactLabel(name) {
4893
+ const lc = name.toLowerCase();
4894
+ const colon = lc.indexOf(":");
4895
+ if (colon < 0) return PLAIN_ABBREVS[lc] ?? name.slice(0, 5);
4896
+ const key = lc.slice(0, colon);
4897
+ const val = name.slice(colon + 1);
4898
+ if (key === "size") return val.slice(0, 3).toUpperCase();
4899
+ if (key === "priority") return `p:${val.slice(0, 1).toUpperCase()}`;
4900
+ if (key === "work") return "WIP";
4901
+ return `${key.slice(0, 2)}:${val.slice(0, 2)}`;
4876
4902
  }
4877
4903
  function labelColor(name) {
4878
- return LABEL_COLORS[name.toLowerCase()] ?? "cyan";
4879
- }
4880
- function IssueRow({ issue, selfLogin, isSelected }) {
4904
+ const lc = name.toLowerCase();
4905
+ if (lc === "bug" || lc === "urgent" || lc.startsWith("priority:h") || lc.startsWith("priority:c"))
4906
+ return "red";
4907
+ if (lc.startsWith("priority:m") || lc.startsWith("work:")) return "yellow";
4908
+ if (lc.startsWith("priority:l") || lc === "wontfix") return "gray";
4909
+ if (lc.startsWith("size:")) return "white";
4910
+ if (lc === "feature" || lc === "enhancement") return "green";
4911
+ if (lc === "documentation") return "blue";
4912
+ if (lc === "good first issue") return "magenta";
4913
+ return "cyan";
4914
+ }
4915
+ function IssueRow({ issue, selfLogin, isSelected, panelWidth }) {
4881
4916
  const assignees = issue.assignees ?? [];
4882
4917
  const isSelf = assignees.some((a) => a.login === selfLogin);
4883
4918
  const isUnassigned = assignees.length === 0;
4884
4919
  const assigneeColor = isSelf ? "green" : isUnassigned ? "gray" : "white";
4885
- const assigneeText = isUnassigned ? "unassigned" : truncate(assignees.map((a) => a.login).join(", "), 14);
4920
+ const assigneeText = isUnassigned ? "unassigned" : truncate(assignees.map((a) => a.login).join(", "), ASSIGN_W);
4886
4921
  const labels = (issue.labels ?? []).slice(0, 2);
4887
- const target = formatTargetDate(issue.targetDate);
4888
- const titleStr = truncate(issue.title, 42).padEnd(42);
4922
+ const date = formatDate2(issue);
4923
+ const innerW = panelWidth - 2;
4924
+ const titleW = Math.max(8, innerW - FIXED_OVERHEAD);
4925
+ const titleStr = truncate(issue.title, titleW).padEnd(titleW);
4926
+ const dateStr = date.text.padStart(DATE_W);
4889
4927
  return /* @__PURE__ */ jsxs21(Box20, { children: [
4890
4928
  isSelected ? /* @__PURE__ */ jsx21(Text19, { color: "cyan", bold: true, children: "\u25B6 " }) : /* @__PURE__ */ jsx21(Text19, { children: " " }),
4891
4929
  /* @__PURE__ */ jsxs21(Text19, { color: "cyan", children: [
@@ -4893,49 +4931,57 @@ function IssueRow({ issue, selfLogin, isSelected }) {
4893
4931
  String(issue.number).padEnd(5)
4894
4932
  ] }),
4895
4933
  /* @__PURE__ */ jsx21(Text19, { children: " " }),
4896
- isSelected ? /* @__PURE__ */ jsx21(Text19, { color: "white", bold: true, children: titleStr }) : /* @__PURE__ */ jsx21(Text19, { children: titleStr }),
4934
+ isSelected ? /* @__PURE__ */ jsx21(Text19, { bold: true, color: "white", children: titleStr }) : /* @__PURE__ */ jsx21(Text19, { children: titleStr }),
4897
4935
  /* @__PURE__ */ jsx21(Text19, { children: " " }),
4898
- /* @__PURE__ */ jsx21(Box20, { width: LABEL_COL_WIDTH, children: labels.map((l, i) => /* @__PURE__ */ jsxs21(Text19, { children: [
4936
+ /* @__PURE__ */ jsx21(Box20, { width: LABEL_W, overflow: "hidden", children: labels.length === 0 ? /* @__PURE__ */ jsx21(Text19, { color: "gray", children: " ".repeat(LABEL_W) }) : labels.map((l, i) => /* @__PURE__ */ jsxs21(Text19, { children: [
4899
4937
  i > 0 ? " " : "",
4900
4938
  /* @__PURE__ */ jsxs21(Text19, { color: labelColor(l.name), children: [
4901
4939
  "[",
4902
- truncate(l.name, 12),
4940
+ compactLabel(l.name),
4903
4941
  "]"
4904
4942
  ] })
4905
4943
  ] }, l.name)) }),
4906
4944
  /* @__PURE__ */ jsx21(Text19, { children: " " }),
4907
- /* @__PURE__ */ jsx21(Text19, { color: assigneeColor, children: assigneeText.padEnd(14) }),
4945
+ /* @__PURE__ */ jsx21(Text19, { color: assigneeColor, children: assigneeText.padEnd(ASSIGN_W) }),
4908
4946
  /* @__PURE__ */ jsx21(Text19, { children: " " }),
4909
- /* @__PURE__ */ jsx21(Text19, { color: "gray", children: timeAgo2(issue.updatedAt).padStart(4) }),
4910
- target.text ? /* @__PURE__ */ jsxs21(Fragment3, { children: [
4911
- /* @__PURE__ */ jsx21(Text19, { children: " " }),
4912
- /* @__PURE__ */ jsx21(Text19, { color: target.color, children: target.text })
4913
- ] }) : null
4947
+ /* @__PURE__ */ jsx21(Text19, { color: date.color, children: dateStr })
4914
4948
  ] });
4915
4949
  }
4916
- var LABEL_COLORS, LABEL_COL_WIDTH;
4950
+ var PLAIN_ABBREVS, CURSOR_W, NUM_W, LABEL_W, ASSIGN_W, DATE_W, FIXED_OVERHEAD;
4917
4951
  var init_issue_row = __esm({
4918
4952
  "src/board/components/issue-row.tsx"() {
4919
4953
  "use strict";
4920
- LABEL_COLORS = {
4921
- bug: "red",
4922
- enhancement: "green",
4923
- feature: "green",
4924
- documentation: "blue",
4925
- "good first issue": "magenta",
4926
- help: "yellow",
4927
- question: "yellow",
4928
- urgent: "red",
4929
- wontfix: "gray"
4954
+ PLAIN_ABBREVS = {
4955
+ bug: "bug",
4956
+ feature: "feat",
4957
+ enhancement: "enh",
4958
+ documentation: "docs",
4959
+ "good first issue": "gfi",
4960
+ help: "help",
4961
+ question: "?",
4962
+ urgent: "urg!",
4963
+ wontfix: "wont",
4964
+ task: "task"
4930
4965
  };
4931
- LABEL_COL_WIDTH = 30;
4966
+ CURSOR_W = 2;
4967
+ NUM_W = 7;
4968
+ LABEL_W = 13;
4969
+ ASSIGN_W = 10;
4970
+ DATE_W = 10;
4971
+ FIXED_OVERHEAD = CURSOR_W + NUM_W + 1 + LABEL_W + 1 + ASSIGN_W + 1 + DATE_W;
4932
4972
  }
4933
4973
  });
4934
4974
 
4935
4975
  // src/board/components/row-renderer.tsx
4936
4976
  import { Box as Box21, Text as Text20 } from "ink";
4937
4977
  import { jsx as jsx22, jsxs as jsxs22 } from "react/jsx-runtime";
4938
- function RowRenderer({ row, selectedId, selfLogin, isMultiSelected }) {
4978
+ function RowRenderer({
4979
+ row,
4980
+ selectedId,
4981
+ selfLogin,
4982
+ isMultiSelected,
4983
+ panelWidth = 120
4984
+ }) {
4939
4985
  switch (row.type) {
4940
4986
  case "sectionHeader": {
4941
4987
  const arrow = row.isCollapsed ? "\u25B6" : "\u25BC";
@@ -4990,7 +5036,15 @@ function RowRenderer({ row, selectedId, selfLogin, isMultiSelected }) {
4990
5036
  const checkbox2 = isMultiSelected != null ? isMultiSelected ? "\u2611 " : "\u2610 " : "";
4991
5037
  return /* @__PURE__ */ jsxs22(Box21, { children: [
4992
5038
  checkbox2 ? /* @__PURE__ */ jsx22(Text20, { color: isMultiSelected ? "cyan" : "gray", children: checkbox2 }) : null,
4993
- /* @__PURE__ */ jsx22(IssueRow, { issue: row.issue, selfLogin, isSelected: selectedId === row.navId })
5039
+ /* @__PURE__ */ jsx22(
5040
+ IssueRow,
5041
+ {
5042
+ issue: row.issue,
5043
+ selfLogin,
5044
+ isSelected: selectedId === row.navId,
5045
+ panelWidth
5046
+ }
5047
+ )
4994
5048
  ] });
4995
5049
  }
4996
5050
  case "activity": {
@@ -5065,10 +5119,10 @@ var init_statuses_panel = __esm({
5065
5119
  // src/board/components/toast-container.tsx
5066
5120
  import { Spinner as Spinner3 } from "@inkjs/ui";
5067
5121
  import { Box as Box23, Text as Text22 } from "ink";
5068
- import { Fragment as Fragment4, jsx as jsx24, jsxs as jsxs24 } from "react/jsx-runtime";
5122
+ import { Fragment as Fragment3, jsx as jsx24, jsxs as jsxs24 } from "react/jsx-runtime";
5069
5123
  function ToastContainer({ toasts }) {
5070
5124
  if (toasts.length === 0) return null;
5071
- return /* @__PURE__ */ jsx24(Box23, { flexDirection: "column", children: toasts.map((t) => /* @__PURE__ */ jsx24(Box23, { children: t.type === "loading" ? /* @__PURE__ */ jsxs24(Fragment4, { children: [
5125
+ return /* @__PURE__ */ jsx24(Box23, { flexDirection: "column", children: toasts.map((t) => /* @__PURE__ */ jsx24(Box23, { children: t.type === "loading" ? /* @__PURE__ */ jsxs24(Fragment3, { children: [
5072
5126
  /* @__PURE__ */ jsx24(Spinner3, { label: "" }),
5073
5127
  /* @__PURE__ */ jsxs24(Text22, { color: "cyan", children: [
5074
5128
  " ",
@@ -5104,7 +5158,7 @@ import { execFileSync as execFileSync3, spawnSync as spawnSync4 } from "child_pr
5104
5158
  import { Spinner as Spinner4 } from "@inkjs/ui";
5105
5159
  import { Box as Box24, Text as Text23, useApp, useStdout } from "ink";
5106
5160
  import { useCallback as useCallback12, useEffect as useEffect9, useMemo as useMemo3, useRef as useRef13, useState as useState16 } from "react";
5107
- import { Fragment as Fragment5, jsx as jsx25, jsxs as jsxs25 } from "react/jsx-runtime";
5161
+ import { Fragment as Fragment4, jsx as jsx25, jsxs as jsxs25 } from "react/jsx-runtime";
5108
5162
  function resolveStatusGroups(statusOptions, configuredGroups) {
5109
5163
  if (configuredGroups && configuredGroups.length > 0) {
5110
5164
  return configuredGroups.map((entry) => {
@@ -5740,7 +5794,8 @@ function Dashboard({ config: config2, options, activeProfile }) {
5740
5794
  activityNav,
5741
5795
  onRepoEnter,
5742
5796
  onStatusEnter,
5743
- onActivityEnter
5797
+ onActivityEnter,
5798
+ showDetailPanel
5744
5799
  });
5745
5800
  if (status === "loading" && !data) {
5746
5801
  return /* @__PURE__ */ jsx25(Box24, { flexDirection: "column", padding: 1, children: /* @__PURE__ */ jsx25(Spinner4, { label: "Loading dashboard..." }) });
@@ -5801,6 +5856,7 @@ function Dashboard({ config: config2, options, activeProfile }) {
5801
5856
  row,
5802
5857
  selectedId: nav.selectedId,
5803
5858
  selfLogin: config2.board.assignee,
5859
+ panelWidth: issuesPanelWidth,
5804
5860
  isMultiSelected: ui.state.mode === "multiSelect" && row.navId ? multiSelect.isSelected(row.navId) : void 0
5805
5861
  },
5806
5862
  row.key
@@ -5851,10 +5907,10 @@ function Dashboard({ config: config2, options, activeProfile }) {
5851
5907
  dateStr
5852
5908
  ] }),
5853
5909
  /* @__PURE__ */ jsx25(Text23, { children: " " }),
5854
- isRefreshing ? /* @__PURE__ */ jsxs25(Fragment5, { children: [
5910
+ isRefreshing ? /* @__PURE__ */ jsxs25(Fragment4, { children: [
5855
5911
  /* @__PURE__ */ jsx25(Spinner4, { label: "" }),
5856
5912
  /* @__PURE__ */ jsx25(Text23, { color: "cyan", children: " Refreshing..." })
5857
- ] }) : /* @__PURE__ */ jsxs25(Fragment5, { children: [
5913
+ ] }) : /* @__PURE__ */ jsxs25(Fragment4, { children: [
5858
5914
  /* @__PURE__ */ jsx25(RefreshAge, { lastRefresh }),
5859
5915
  consecutiveFailures > 0 ? /* @__PURE__ */ jsx25(Text23, { color: "red", children: " (!)" }) : null
5860
5916
  ] }),
@@ -5906,7 +5962,18 @@ function Dashboard({ config: config2, options, activeProfile }) {
5906
5962
  onPushEntry: pushEntry
5907
5963
  }
5908
5964
  ),
5909
- !ui.state.helpVisible && ui.state.mode !== "overlay:status" && ui.state.mode !== "overlay:create" && ui.state.mode !== "overlay:createNl" && ui.state.mode !== "overlay:bulkAction" && ui.state.mode !== "overlay:confirmPick" && ui.state.mode !== "focus" ? /* @__PURE__ */ jsx25(
5965
+ ui.state.mode === "overlay:detail" ? /* @__PURE__ */ jsx25(
5966
+ DetailPanel,
5967
+ {
5968
+ issue: selectedItem.issue,
5969
+ width: termSize.cols,
5970
+ isActive: true,
5971
+ issueRepo: selectedItem.repoName,
5972
+ fetchComments: handleFetchComments,
5973
+ commentsState: currentCommentsState
5974
+ }
5975
+ ) : null,
5976
+ !ui.state.helpVisible && ui.state.mode !== "overlay:status" && ui.state.mode !== "overlay:create" && ui.state.mode !== "overlay:createNl" && ui.state.mode !== "overlay:bulkAction" && ui.state.mode !== "overlay:confirmPick" && ui.state.mode !== "overlay:detail" && ui.state.mode !== "focus" ? /* @__PURE__ */ jsx25(
5910
5977
  PanelLayout,
5911
5978
  {
5912
5979
  cols: termSize.cols,
@@ -7142,7 +7209,7 @@ function resolveProjectId(projectId) {
7142
7209
  process.exit(1);
7143
7210
  }
7144
7211
  var program = new Command();
7145
- program.name("hog").description("Personal command deck \u2014 unified task dashboard for GitHub Projects + TickTick").version("1.12.0").option("--json", "Force JSON output").option("--human", "Force human-readable output").hook("preAction", (thisCommand) => {
7212
+ program.name("hog").description("Personal command deck \u2014 unified task dashboard for GitHub Projects + TickTick").version("1.14.0").option("--json", "Force JSON output").option("--human", "Force human-readable output").hook("preAction", (thisCommand) => {
7146
7213
  const opts = thisCommand.opts();
7147
7214
  if (opts.json) setFormat("json");
7148
7215
  if (opts.human) setFormat("human");