@kud/jira-ink 0.7.0 → 0.9.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.
Files changed (3) hide show
  1. package/dist/index.d.ts +292 -104
  2. package/dist/index.js +494 -233
  3. package/package.json +1 -1
package/dist/index.d.ts CHANGED
@@ -2,102 +2,7 @@ import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
3
  import { MarkdownTheme } from '@kud/ink-markdown';
4
4
  import { Hint } from '@kud/ink-ui';
5
- import { LocatedAttachment, JiraClient, BoardTab as BoardTab$1, JiraBoardConfiguration, JiraIssue, SearchMode } from '@kud/jira';
6
-
7
- /**
8
- * One issue, as the detail screen reads it: every field already a string, ADF
9
- * already Markdown, attachments already located. The view never touches a raw
10
- * `JiraIssue`, so a host that fakes a `JiraClient` for a test or a screenshot
11
- * gets a screen it can predict, and one that fetched the issue by some other
12
- * route can build this shape by hand.
13
- */
14
- type IssueDetail = {
15
- key: string;
16
- summary: string;
17
- status: string;
18
- type: string;
19
- assignee: string;
20
- reporter: string;
21
- labels: string[];
22
- parent?: {
23
- key: string;
24
- summary: string;
25
- };
26
- url: string;
27
- description: string;
28
- comments: {
29
- id: string;
30
- author: string;
31
- created: string;
32
- body: string;
33
- }[];
34
- attachments: LocatedAttachment[];
35
- };
36
- type Transition = {
37
- id: string;
38
- name: string;
39
- to: string;
40
- };
41
- declare const issueDetailOf: (client: JiraClient, baseUrl: string, key: string) => Promise<IssueDetail>;
42
- declare const transitionsOf: (client: JiraClient, key: string) => Promise<Transition[]>;
43
-
44
- type IssueDetailViewProps = {
45
- issue: IssueDetail;
46
- height: number;
47
- width: number;
48
- onBack: () => void;
49
- /**
50
- * The write actions are optional, and a key whose handler is absent is
51
- * neither bound nor hinted. A host that only READS — cockpit's ticket drill —
52
- * mounts the same screen jira-cli's TUI does, minus the verbs it has no way
53
- * to honour, rather than a hint row promising `t` and `c` that do nothing.
54
- */
55
- onTransition?: () => void;
56
- onComment?: () => void;
57
- onAssign?: () => void;
58
- onOpenBrowser?: () => void;
59
- /**
60
- * A host that draws its own chrome — a bordered drill frame with the title
61
- * on top and the hints pinned at the foot — takes the screen in parts and
62
- * places them. The view then draws neither its title line nor its footer,
63
- * so nothing appears twice. `height` is still the whole area the host has
64
- * given the body, frame excluded.
65
- */
66
- frame?: (parts: {
67
- title: string;
68
- subtitle: string;
69
- hints: Hint[];
70
- body: ReactNode;
71
- }) => ReactNode;
72
- /** Colours for the Markdown body; a host passes its own tokens so links, code and headings sit in its palette. */
73
- theme?: MarkdownTheme;
74
- };
75
- declare const IssueDetailView: ({ issue, height, width, onBack, onTransition, onComment, onAssign, onOpenBrowser, frame, theme, }: IssueDetailViewProps) => string | number | bigint | boolean | Iterable<ReactNode> | Promise<string | number | bigint | boolean | react.ReactPortal | react.ReactElement<unknown, string | react.JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined> | react.JSX.Element | null | undefined;
76
-
77
- type IssueBodyProps = {
78
- client: JiraClient;
79
- /** Instance URL, as `loadConfig` resolves it; only used to build the browser link. */
80
- baseUrl: string;
81
- issueKey: string;
82
- onExit: () => void;
83
- /** Override the terminal-derived size when the host draws inside a frame. */
84
- width?: number;
85
- height?: number;
86
- /** The host's chrome around the screen — see `IssueDetailView`. */
87
- frame?: IssueDetailViewProps["frame"];
88
- theme?: IssueDetailViewProps["theme"];
89
- };
90
- /**
91
- * The issue screen as a host mounts it: hand it a client and a key, and it
92
- * fetches, draws and reads its own keys until `onExit`. `JenkinsBody`'s shape,
93
- * for the same reason — a cockpit mounting a ticket over its list should not
94
- * have to know how an issue is fetched or what the loading states look like.
95
- *
96
- * Read-only by construction. The write verbs (`t`, `c`, `a`) exist on
97
- * `IssueDetailView` for jira-cli's own TUI, which owns the prompts they need;
98
- * here they are simply not wired, so the hint row does not offer them.
99
- */
100
- declare const IssueBody: ({ client, baseUrl, issueKey, onExit, width, height, frame, theme, }: IssueBodyProps) => string | number | bigint | boolean | Iterable<ReactNode> | Promise<string | number | bigint | boolean | react.ReactPortal | react.ReactElement<unknown, string | react.JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined> | react.JSX.Element | null | undefined;
5
+ import { BoardTab as BoardTab$1, JiraClient, JiraBoardConfiguration, JiraIssue, LocatedAttachment, SearchMode } from '@kud/jira';
101
6
 
102
7
  /** Jira's own three, plus `unknown` for a status that arrives without one. */
103
8
  type StatusCategory = "new" | "indeterminate" | "done" | "unknown";
@@ -124,6 +29,22 @@ type BoardRow = {
124
29
  assignee: string;
125
30
  /** ISO timestamp, kept whole so the view can draw a relative age. */
126
31
  updated: string;
32
+ /**
33
+ * Set when the row's own status lags the evidence — an epic Off board with
34
+ * a child already started, a task whose PR is ahead of it. The board draws
35
+ * the word `behind` bright, then this string dim as the reason; pass `""`
36
+ * when the head's own note already says why. **Presence is the flag, so
37
+ * test `behind !== undefined`, never `if (row.behind)`.**
38
+ *
39
+ * Host-filled, because only the host knows the evidence outside Jira.
40
+ * `withBehind` fills the two cases the board can see for itself.
41
+ */
42
+ behind?: string;
43
+ /**
44
+ * A transition in flight: the label of the tab it is moving to, drawn as
45
+ * `⋯ → QA` until `transitionRow` or `settleRow` clears it.
46
+ */
47
+ pending?: string;
127
48
  };
128
49
  declare const toBoardRow: (issue: JiraIssue) => BoardRow;
129
50
  /**
@@ -157,7 +78,44 @@ declare const tabsFromBoard: (configuration: JiraBoardConfiguration) => BoardTab
157
78
  declare const tabsFromCategories: () => BoardTabs;
158
79
  /** The tab a row belongs to. Nothing ever falls out: unclaimed rows go Off board, or In progress on a category board. */
159
80
  declare const tabOf: (row: BoardRow, { tabs }: BoardTabs) => string;
160
- declare const countsFor: (rows: BoardRow[], tabs: BoardTabs) => Record<string, number>;
81
+ type Placement = {
82
+ /** The tabs a row is drawn in, in tab order. A container can be in several. */
83
+ tabsOf: (key: string) => string[];
84
+ /** The rows the board holds that call this one parent. */
85
+ childrenOf: (key: string) => BoardRow[];
86
+ };
87
+ /**
88
+ * Where every row goes, read BOTTOM-UP. A task sits in the tab of its own
89
+ * status; a container is placed by its CHILDREN, heading a group in each tab
90
+ * that holds one — so an epic In Development whose tasks are all in Backlog
91
+ * is absent from In progress and heads them Off board. Its own status never
92
+ * picks a tab; the board says it in a note instead.
93
+ *
94
+ * A container with no children on the board has nothing to be placed under,
95
+ * and sits Off board rather than claiming a stage tab it is not working in.
96
+ */
97
+ declare const placementOf: (rows: BoardRow[], tabs: BoardTabs) => Placement;
98
+ /**
99
+ * The rows one tab draws: the tasks whose own status is here, and the
100
+ * container heading each of them. One filter does both halves, because a
101
+ * container's placement already names every tab it has a child in.
102
+ */
103
+ declare const rowsInTab: (rows: BoardRow[], tabs: BoardTabs, tab: string, placement?: Placement) => BoardRow[];
104
+ /**
105
+ * The dim segments after a head's summary: its own status when it differs
106
+ * from this tab, then where the rest of its children are, with counts. Only
107
+ * rows the board holds are counted — never the epic-children query.
108
+ */
109
+ declare const headNote: (row: BoardRow, tabs: BoardTabs, tab: string, placement: Placement) => string | undefined;
110
+ /**
111
+ * Fills `behind` for the two cases the board can see without leaving Jira:
112
+ * an epic parked Off board while a child has already started, and an epic
113
+ * still open when every child is Done. Both reasons are left empty — the
114
+ * head's own note already names the status that lags. A `behind` the host
115
+ * has already set is never overwritten.
116
+ */
117
+ declare const withBehind: (rows: BoardRow[], tabs: BoardTabs) => BoardRow[];
118
+ declare const countsFor: (rows: BoardRow[], tabs: BoardTabs, placement?: Placement) => Record<string, number>;
161
119
  /** Tabs worth drawing: every configured one, plus Off board only when it holds something. */
162
120
  declare const visibleTabs: (tabs: BoardTabs, counts: Record<string, number>) => BoardTab[];
163
121
  /**
@@ -168,21 +126,85 @@ declare const visibleTabs: (tabs: BoardTabs, counts: Record<string, number>) =>
168
126
  * with no parent — and each group after the first is preceded by a blank
169
127
  * line. A tab where nothing has a parent and nothing is a container degrades
170
128
  * to a plain list, never to a lone fence.
129
+ *
130
+ * A LEAF is something the host hangs under a row that is not a Jira issue —
131
+ * cockpit hangs a task's pull requests there. The board never looks inside
132
+ * one: `L` is opaque, the host renders it and opens it. A leaf sits one
133
+ * level under its row, directly beneath it and before any children, so a
134
+ * row's subtree is always one contiguous run. Leaves are cursor stops like
135
+ * issues; fences and gaps never are.
171
136
  */
172
- type Block = {
137
+ type Block<L = never> = {
173
138
  kind: "gap";
174
139
  } | {
175
140
  kind: "fence";
176
141
  key: string | null;
177
142
  summary: string;
143
+ parent?: BoardRow;
178
144
  } | {
179
145
  kind: "issue";
180
146
  row: BoardRow;
181
147
  depth: 0 | 1;
148
+ } | {
149
+ kind: "leaf";
150
+ parent: BoardRow;
151
+ depth: 1 | 2;
152
+ data: L;
153
+ };
154
+ type Stop<L = never> = Extract<Block<L>, {
155
+ kind: "issue" | "leaf";
156
+ }>;
157
+ declare const isStop: <L>(block: Block<L>) => block is Stop<L>;
158
+ /** The rows and leaves in cursor order — what ↑↓ walks and ↵ opens. */
159
+ declare const stopsOf: <L>(blocks: Block<L>[]) => Stop<L>[];
160
+ declare const blocksFor: <L = never>(rows: BoardRow[], under?: (row: BoardRow) => L[],
161
+ /**
162
+ * Parents the board holds no row for, looked up by the host — jira with one
163
+ * extra `key in (…)` search, cockpit with the foreign-epics pass it already
164
+ * runs. They are never drawn as rows; they tell a fence where its parent is.
165
+ */
166
+ parents?: BoardRow[]) => Block<L>[];
167
+ /**
168
+ * What a fence says after the parent's name, and whether it carries `behind`.
169
+ *
170
+ * Where the parent IS says it best: a tab label when the reader can press
171
+ * through and find it, the OWNER when they cannot — `@` being the house glyph
172
+ * for scope, as in the title bar's `@you`. On a `mine` board those two are
173
+ * the same question asked twice, so a tab name means yours and `@name` means
174
+ * not yours, and no fence ever says both. A board showing several assignees
175
+ * has no such shorthand and always names the owner, as its rows do.
176
+ *
177
+ * `behind` is a judgement on someone's own work, so it goes on a fence only
178
+ * when that someone is the viewer.
179
+ *
180
+ * Empty until the lookup lands: a fence that guessed would be wrong for a
181
+ * frame and then silently correct itself, which is worse than a blank.
182
+ */
183
+ declare const fenceNote: (parent: BoardRow | undefined, tabs: BoardTabs, { viewer, mine }: {
184
+ viewer: string;
185
+ mine: boolean;
186
+ }) => {
187
+ segment: string;
188
+ behind: boolean;
182
189
  };
183
- declare const blocksFor: (rows: BoardRow[]) => Block[];
184
- /** Index into `blocks` of the n-th issue, so a cursor over issues maps to a line. */
185
- declare const blockIndexOfIssue: (blocks: Block[], issueIndex: number) => number;
190
+ /** Index into `blocks` of the n-th stop, so a cursor over stops maps to a line. */
191
+ declare const blockIndexOfStop: <L>(blocks: Block<L>[], stopIndex: number) => number;
192
+ /**
193
+ * The tree glyphs to the left of the stop at `i`, three columns per level:
194
+ * `├─ ` while a sibling still follows, `└─ ` for the last, and ahead of a
195
+ * depth-2 connector a `│ ` stem while its parent is not last. A `└─` on
196
+ * every child was tried in gh-ink and rejected — three closing corners in a
197
+ * column draw no tree at all — which is why last-ness is tracked here
198
+ * rather than left to the renderer.
199
+ */
200
+ declare const treePrefix: <L>(blocks: Block<L>[], i: number) => string;
201
+ /**
202
+ * The block at `i` and everything hanging under it: a row's own leaves, its
203
+ * children and theirs. Pure, so a host can walk a subtree for every URL in
204
+ * it (`C` / `O`) without knowing how the tree was laid out. A leaf, a fence
205
+ * or a gap has no subtree beyond itself.
206
+ */
207
+ declare const subtreeOf: <L>(blocks: Block<L>[], i: number) => Block<L>[];
186
208
  type PriorityGlyph = "⇈" | "↑" | "=" | "↓" | "⇊" | " ";
187
209
  /**
188
210
  * Rank against the scheme's default rung, in Jira's own arrow grammar: two
@@ -208,6 +230,26 @@ type BoardModel = {
208
230
  rows: BoardRow[];
209
231
  tabs: BoardTabs;
210
232
  };
233
+ /** A transition's target, as Jira gives it: id, name and category together. */
234
+ type TransitionTarget = {
235
+ id?: string;
236
+ name: string;
237
+ category?: StatusCategory;
238
+ };
239
+ /** The label of the tab a status lands in — what `⋯ → QA` names. */
240
+ declare const tabLabelOf: (to: TransitionTarget, tabs: BoardTabs) => string;
241
+ /**
242
+ * The hybrid move, in three pure steps. The row stays where it is under a
243
+ * `⋯ → QA` marker while the request is out; on the 204 its status is
244
+ * rewritten here and the tab follows within the same second, long before the
245
+ * board refetches — the refetch stays as reconciliation, never as the thing
246
+ * that makes the move visible. Nothing is optimistic: the row moves because
247
+ * Jira said it moved.
248
+ */
249
+ declare const pendingRow: (model: BoardModel, key: string, to: TransitionTarget) => BoardModel;
250
+ declare const transitionRow: (model: BoardModel, key: string, to: TransitionTarget) => BoardModel;
251
+ /** Clears the marker without moving anything — what a rejected move leaves. */
252
+ declare const settleRow: (model: BoardModel, key: string) => BoardModel;
211
253
  type BoardOptions = {
212
254
  /** Hand-written tabs from the config file — wins over everything. */
213
255
  tabs?: BoardTab$1[];
@@ -215,6 +257,16 @@ type BoardOptions = {
215
257
  board?: number;
216
258
  limit?: number;
217
259
  };
260
+ /**
261
+ * The parents the board holds no row for, in ONE `key in (…)` search — an
262
+ * epic assigned to someone else, or one that fell outside the window while
263
+ * its child stayed open. They are never drawn as rows; they are what lets a
264
+ * fence say whose epic it is, which is the whole of their job here.
265
+ *
266
+ * Empty in, empty out, and no call made: a board of unparented rows must not
267
+ * pay for a search that can only return nothing.
268
+ */
269
+ declare const parentsOf: (client: JiraClient, rows: BoardRow[]) => Promise<BoardRow[]>;
218
270
  /**
219
271
  * One assignee-scoped query, bucketed client-side. The board supplies the
220
272
  * VOCABULARY, never the population — `getBoardIssues` would drop your
@@ -223,6 +275,119 @@ type BoardOptions = {
223
275
  */
224
276
  declare const boardOf: (client: JiraClient, jql: string, { tabs, board, limit }?: BoardOptions) => Promise<BoardModel>;
225
277
 
278
+ /**
279
+ * One issue, as the detail screen reads it: every field already a string, ADF
280
+ * already Markdown, attachments already located. The view never touches a raw
281
+ * `JiraIssue`, so a host that fakes a `JiraClient` for a test or a screenshot
282
+ * gets a screen it can predict, and one that fetched the issue by some other
283
+ * route can build this shape by hand.
284
+ */
285
+ type IssueDetail = {
286
+ key: string;
287
+ summary: string;
288
+ status: string;
289
+ type: string;
290
+ assignee: string;
291
+ reporter: string;
292
+ labels: string[];
293
+ /** Jira's priority name, verbatim; absent when the field is off. */
294
+ priority?: string;
295
+ parent?: {
296
+ key: string;
297
+ summary: string;
298
+ };
299
+ url: string;
300
+ description: string;
301
+ comments: {
302
+ id: string;
303
+ author: string;
304
+ created: string;
305
+ body: string;
306
+ }[];
307
+ attachments: LocatedAttachment[];
308
+ };
309
+ /**
310
+ * A transition and where it lands. `to` carries the whole status — id, name
311
+ * and category — because a host that must decide which TAB the move ends in
312
+ * cannot do it from a name: names are per-instance, and matching on one is
313
+ * how a transition table once silently stopped matching anything.
314
+ */
315
+ type Transition = {
316
+ id: string;
317
+ name: string;
318
+ to: {
319
+ id?: string;
320
+ name: string;
321
+ category?: StatusCategory;
322
+ };
323
+ };
324
+ declare const issueDetailOf: (client: JiraClient, baseUrl: string, key: string) => Promise<IssueDetail>;
325
+ declare const transitionsOf: (client: JiraClient, key: string) => Promise<Transition[]>;
326
+
327
+ type IssueDetailViewProps = {
328
+ issue: IssueDetail;
329
+ height: number;
330
+ width: number;
331
+ onBack: () => void;
332
+ /**
333
+ * The write actions are optional, and a key whose handler is absent is
334
+ * neither bound nor hinted. A host that only READS — cockpit's ticket drill —
335
+ * mounts the same screen jira-cli's TUI does, minus the verbs it has no way
336
+ * to honour, rather than a hint row promising `t` and `c` that do nothing.
337
+ */
338
+ onTransition?: () => void;
339
+ onComment?: () => void;
340
+ onAssign?: () => void;
341
+ onOpenBrowser?: () => void;
342
+ /**
343
+ * A host that draws its own chrome — a bordered drill frame with the title
344
+ * on top and the hints pinned at the foot — takes the screen in parts and
345
+ * places them. The view then draws neither its title line nor its footer,
346
+ * so nothing appears twice. `height` is still the whole area the host has
347
+ * given the body, frame excluded.
348
+ */
349
+ frame?: (parts: {
350
+ title: string;
351
+ subtitle: string;
352
+ hints: Hint[];
353
+ body: ReactNode;
354
+ }) => ReactNode;
355
+ /** Colours for the Markdown body; a host passes its own tokens so links, code and headings sit in its palette. */
356
+ theme?: MarkdownTheme;
357
+ };
358
+ declare const IssueDetailView: ({ issue, height, width, onBack, onTransition, onComment, onAssign, onOpenBrowser, frame, theme, }: IssueDetailViewProps) => string | number | bigint | boolean | Iterable<ReactNode> | Promise<string | number | bigint | boolean | react.ReactPortal | react.ReactElement<unknown, string | react.JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined> | react.JSX.Element | null | undefined;
359
+
360
+ type IssueBodyProps = {
361
+ client: JiraClient;
362
+ /** Instance URL, as `loadConfig` resolves it; only used to build the browser link. */
363
+ baseUrl: string;
364
+ issueKey: string;
365
+ /**
366
+ * The summary as the host already knows it — from the list row that was
367
+ * opened — so the frame carries the title from the first frame rather than
368
+ * only once the fetch answers. The fetched summary wins once it is here.
369
+ */
370
+ summary?: string;
371
+ onExit: () => void;
372
+ /** Override the terminal-derived size when the host draws inside a frame. */
373
+ width?: number;
374
+ height?: number;
375
+ /** The host's chrome around the screen — see `IssueDetailView`. */
376
+ frame?: IssueDetailViewProps["frame"];
377
+ theme?: IssueDetailViewProps["theme"];
378
+ };
379
+ /**
380
+ * The issue screen as a host mounts it: hand it a client and a key, and it
381
+ * fetches, draws and reads its own keys until `onExit`. `JenkinsBody`'s shape,
382
+ * for the same reason — a cockpit mounting a ticket over its list should not
383
+ * have to know how an issue is fetched or what the loading states look like.
384
+ *
385
+ * Read-only by construction. The write verbs (`t`, `c`, `a`) exist on
386
+ * `IssueDetailView` for jira-cli's own TUI, which owns the prompts they need;
387
+ * here they are simply not wired, so the hint row does not offer them.
388
+ */
389
+ declare const IssueBody: ({ client, baseUrl, issueKey, summary, onExit, width, height, frame, theme, }: IssueBodyProps) => string | number | bigint | boolean | Iterable<ReactNode> | Promise<string | number | bigint | boolean | react.ReactPortal | react.ReactElement<unknown, string | react.JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined> | react.JSX.Element | null | undefined;
390
+
226
391
  /** What the board is showing, named in the title row. */
227
392
  type BoardScope = {
228
393
  kind: "mine";
@@ -231,8 +396,31 @@ type BoardScope = {
231
396
  query: string;
232
397
  mode: "jql" | "text";
233
398
  };
234
- type IssueBoardProps = {
399
+ /**
400
+ * What a host hangs under rows that is not a Jira issue — cockpit hangs a
401
+ * task's pull requests. The board draws the cursor marker and the tree cell
402
+ * and hands the rest of the line to `render`; `width` is the columns left
403
+ * after those, so the renderer can truncate honestly. `L` never reaches
404
+ * the board's own logic: this package stays Jira-only.
405
+ */
406
+ type BoardLeaves<L> = {
407
+ under: (row: BoardRow) => L[];
408
+ keyOf: (leaf: L) => string;
409
+ render: (leaf: L, ctx: {
410
+ active: boolean;
411
+ width: number;
412
+ }) => ReactNode;
413
+ onOpen: (leaf: L) => void;
414
+ };
415
+ type IssueBoardProps<L = never> = {
235
416
  model: BoardModel;
417
+ leaves?: BoardLeaves<L>;
418
+ /**
419
+ * Parents the board holds no row for, so a fence can say where its parent
420
+ * is. Absent until the lookup lands, which leaves the fence bare rather
421
+ * than guessing.
422
+ */
423
+ parents?: BoardRow[];
236
424
  /** Who the `mine` scope belongs to, for the title row. */
237
425
  viewer: string;
238
426
  loadedAt: number;
@@ -289,7 +477,7 @@ type IssueBoardProps = {
289
477
  * host — only the keys that open and close the layers it pushes itself: the
290
478
  * search input and the legend.
291
479
  */
292
- declare const IssueBoard: ({ model, viewer, loadedAt, scope, showingAll, searchError, width, height, onOpen, onToggleAll, onRefresh, onSearch, onClearSearch, frame, onInputFocus, }: IssueBoardProps) => string | number | bigint | boolean | Iterable<ReactNode> | Promise<string | number | bigint | boolean | react.ReactPortal | react.ReactElement<unknown, string | react.JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined> | react.JSX.Element | null | undefined;
480
+ declare const IssueBoard: <L = never>({ model, leaves, parents, viewer, loadedAt, scope, showingAll, searchError, width, height, onOpen, onToggleAll, onRefresh, onSearch, onClearSearch, frame, onInputFocus, }: IssueBoardProps<L>) => string | number | bigint | boolean | Iterable<ReactNode> | Promise<string | number | bigint | boolean | react.ReactPortal | react.ReactElement<unknown, string | react.JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined> | react.JSX.Element | null | undefined;
293
481
 
294
482
  type PriorityMarker = {
295
483
  /** Always one column wide, so a list filling every row keeps its grid. */
@@ -314,4 +502,4 @@ declare const priorityMarker: (name: string | null) => PriorityMarker;
314
502
  declare const MOCK_BOARD_ROWS: BoardRow[];
315
503
  declare const mockBoard: () => BoardModel;
316
504
 
317
- export { type Block, type BoardModel, type BoardOptions, type BoardRow, type BoardScope, type BoardTab, type BoardTabs, IssueBoard, type IssueBoardProps, IssueBody, type IssueBodyProps, type IssueDetail, IssueDetailView, type IssueDetailViewProps, MOCK_BOARD_ROWS, OFF_BOARD, type PriorityGlyph, type PriorityMarker, type StatusCategory, type TabSource, type Transition, blockIndexOfIssue, blocksFor, boardOf, countsFor, issueDetailOf, mockBoard, pillVariantFor, priorityGlyph, priorityMarker, relativeAge, tabOf, tabsFromBoard, tabsFromCategories, tabsFromConfig, toBoardRow, transitionsOf, visibleTabs };
505
+ export { type Block, type BoardLeaves, type BoardModel, type BoardOptions, type BoardRow, type BoardScope, type BoardTab, type BoardTabs, IssueBoard, type IssueBoardProps, IssueBody, type IssueBodyProps, type IssueDetail, IssueDetailView, type IssueDetailViewProps, MOCK_BOARD_ROWS, OFF_BOARD, type Placement, type PriorityGlyph, type PriorityMarker, type StatusCategory, type Stop, type TabSource, type Transition, type TransitionTarget, blockIndexOfStop, blocksFor, boardOf, countsFor, fenceNote, headNote, isStop, issueDetailOf, mockBoard, parentsOf, pendingRow, pillVariantFor, placementOf, priorityGlyph, priorityMarker, relativeAge, rowsInTab, settleRow, stopsOf, subtreeOf, tabLabelOf, tabOf, tabsFromBoard, tabsFromCategories, tabsFromConfig, toBoardRow, transitionRow, transitionsOf, treePrefix, visibleTabs, withBehind };
package/dist/index.js CHANGED
@@ -1,12 +1,345 @@
1
1
  import { MarkdownViewport } from '@kud/ink-markdown';
2
- import { colors, useTabs, KeyValue, Badge, Tabs, FooterHints, StatusMessage, useListCursor, pillWidth, TextInput, SelectableRow, Pill, LoadingScreen } from '@kud/ink-ui';
2
+ import { colors, KeyValue, Badge, FooterHints, StatusMessage, useTabs, useListCursor, pillWidth, TextInput, Tabs, SelectableRow, Pill, LoadingScreen } from '@kud/ink-ui';
3
3
  import { useInput, Box, Text, useStdout } from 'ink';
4
4
  import { useState, useEffect, useMemo } from 'react';
5
+ import { isContainerType, jqlEscape, locateAttachments, adfToMarkdown, looksLikeJql } from '@kud/jira';
5
6
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
6
7
  import { exec } from 'child_process';
7
- import { locateAttachments, adfToMarkdown, isContainerType, looksLikeJql } from '@kud/jira';
8
8
 
9
9
  // src/components/issue-detail-view.tsx
10
+ var CATEGORIES = ["new", "indeterminate", "done"];
11
+ var categoryOf = (key) => CATEGORIES.find((c) => c === key) ?? "unknown";
12
+ var toBoardRow = (issue) => ({
13
+ key: issue.key,
14
+ summary: issue.fields.summary ?? "",
15
+ status: issue.fields.status?.name ?? "\u2014",
16
+ statusId: issue.fields.status?.id ?? "",
17
+ category: categoryOf(issue.fields.status?.statusCategory?.key),
18
+ type: issue.fields.issuetype?.name ?? "",
19
+ container: isContainerType(issue.fields.issuetype),
20
+ priority: issue.fields.priority?.name ?? null,
21
+ ...issue.fields.parent ? {
22
+ parent: {
23
+ key: issue.fields.parent.key,
24
+ summary: issue.fields.parent.fields?.summary ?? ""
25
+ }
26
+ } : {},
27
+ assignee: issue.fields.assignee?.displayName ?? "unassigned",
28
+ updated: issue.fields.updated ?? ""
29
+ });
30
+ var OFF_BOARD = "__off_board__";
31
+ var CATEGORY_TABS = [
32
+ { value: "todo", label: "To do", category: "new" },
33
+ { value: "doing", label: "In progress", category: "indeterminate" },
34
+ { value: "done", label: "Done", category: "done" }
35
+ ];
36
+ var withOffBoard = (tabs) => [
37
+ ...tabs,
38
+ { value: OFF_BOARD, label: "Off board", offBoard: true }
39
+ ];
40
+ var tabsFromConfig = (configured) => ({
41
+ source: "config",
42
+ tabs: withOffBoard(
43
+ configured.map((t, i) => ({
44
+ value: `config:${i}`,
45
+ label: t.label,
46
+ statuses: new Set(t.statuses.map((s) => s.toLowerCase()))
47
+ }))
48
+ )
49
+ });
50
+ var tabsFromBoard = (configuration) => {
51
+ const columns = configuration.columnConfig?.columns ?? [];
52
+ if (columns.length === 0) return null;
53
+ return {
54
+ source: "board",
55
+ tabs: withOffBoard(
56
+ columns.map((c, i) => ({
57
+ value: `column:${i}`,
58
+ label: c.name,
59
+ statuses: new Set(c.statuses.map((s) => s.id))
60
+ }))
61
+ )
62
+ };
63
+ };
64
+ var tabsFromCategories = () => ({
65
+ source: "categories",
66
+ tabs: CATEGORY_TABS
67
+ });
68
+ var tabOf = (row, { tabs }) => {
69
+ const claimed = tabs.find(
70
+ (t) => t.statuses?.has(row.statusId) || t.statuses?.has(row.status.toLowerCase())
71
+ );
72
+ if (claimed) return claimed.value;
73
+ const byCategory = tabs.find((t) => t.category && t.category === row.category);
74
+ if (byCategory) return byCategory.value;
75
+ return tabs.find((t) => t.offBoard)?.value ?? "doing";
76
+ };
77
+ var offBoardValue = (tabs) => tabs.tabs.find((t) => t.offBoard)?.value;
78
+ var placementOf = (rows, tabs) => {
79
+ const children = /* @__PURE__ */ new Map();
80
+ for (const row of rows) {
81
+ if (row.container) continue;
82
+ const key = row.parent?.key;
83
+ if (key) children.set(key, [...children.get(key) ?? [], row]);
84
+ }
85
+ const order = new Map(tabs.tabs.map((t, i) => [t.value, i]));
86
+ const placed = /* @__PURE__ */ new Map();
87
+ for (const row of rows) {
88
+ if (!row.container) {
89
+ placed.set(row.key, [tabOf(row, tabs)]);
90
+ continue;
91
+ }
92
+ const where = [
93
+ ...new Set((children.get(row.key) ?? []).map((k) => tabOf(k, tabs)))
94
+ ].sort((a, b) => (order.get(a) ?? 0) - (order.get(b) ?? 0));
95
+ placed.set(
96
+ row.key,
97
+ where.length ? where : [offBoardValue(tabs) ?? tabOf(row, tabs)]
98
+ );
99
+ }
100
+ return {
101
+ tabsOf: (key) => placed.get(key) ?? [],
102
+ childrenOf: (key) => children.get(key) ?? []
103
+ };
104
+ };
105
+ var rowsInTab = (rows, tabs, tab, placement = placementOf(rows, tabs)) => rows.filter((r) => placement.tabsOf(r.key).includes(tab));
106
+ var headNote = (row, tabs, tab, placement) => {
107
+ if (!row.container) return void 0;
108
+ const labelOf = (value) => tabs.tabs.find((t) => t.value === value)?.label ?? "";
109
+ const own = tabOf(row, tabs);
110
+ const segments = own === tab ? [] : [`own ${labelOf(own)}`];
111
+ const elsewhere = /* @__PURE__ */ new Map();
112
+ for (const kid of placement.childrenOf(row.key)) {
113
+ const where = tabOf(kid, tabs);
114
+ if (where !== tab) elsewhere.set(where, (elsewhere.get(where) ?? 0) + 1);
115
+ }
116
+ for (const t of tabs.tabs) {
117
+ const n = elsewhere.get(t.value);
118
+ if (n) segments.push(`${n} ${t.label}`);
119
+ }
120
+ return segments.length ? segments.join(" \xB7 ") : void 0;
121
+ };
122
+ var withBehind = (rows, tabs) => {
123
+ const placement = placementOf(rows, tabs);
124
+ const off = offBoardValue(tabs);
125
+ return rows.map((row) => {
126
+ if (!row.container || row.behind !== void 0) return row;
127
+ const kids = placement.childrenOf(row.key);
128
+ if (kids.length === 0) return row;
129
+ const started = kids.some((k) => k.category !== "new");
130
+ const allDone = kids.every((k) => k.category === "done");
131
+ const lags = off !== void 0 && tabOf(row, tabs) === off && started || row.category !== "done" && allDone;
132
+ return lags ? { ...row, behind: "" } : row;
133
+ });
134
+ };
135
+ var countsFor = (rows, tabs, placement = placementOf(rows, tabs)) => {
136
+ const counts = Object.fromEntries(
137
+ tabs.tabs.map((t) => [t.value, 0])
138
+ );
139
+ for (const row of rows)
140
+ for (const tab of placement.tabsOf(row.key))
141
+ counts[tab] = (counts[tab] ?? 0) + 1;
142
+ return counts;
143
+ };
144
+ var visibleTabs = (tabs, counts) => tabs.tabs.filter((t) => !t.offBoard || (counts[t.value] ?? 0) > 0);
145
+ var isStop = (block) => block.kind === "issue" || block.kind === "leaf";
146
+ var stopsOf = (blocks) => blocks.filter(isStop);
147
+ var blocksFor = (rows, under, parents) => {
148
+ const known = new Map((parents ?? []).map((p) => [p.key, p]));
149
+ const withLeaves = (row, depth) => [
150
+ { kind: "issue", row, depth },
151
+ ...(under?.(row) ?? []).map((data) => ({
152
+ kind: "leaf",
153
+ parent: row,
154
+ depth: depth === 0 ? 1 : 2,
155
+ data
156
+ }))
157
+ ];
158
+ const hasStructure = rows.some((r) => r.parent || r.container);
159
+ if (!hasStructure) return rows.flatMap((row) => withLeaves(row, 0));
160
+ const heads = new Map(rows.filter((r) => r.container).map((r) => [r.key, r]));
161
+ const children = /* @__PURE__ */ new Map();
162
+ const orphans = [];
163
+ for (const row of rows) {
164
+ if (row.container) continue;
165
+ const key = row.parent?.key;
166
+ if (!key) {
167
+ orphans.push(row);
168
+ continue;
169
+ }
170
+ children.set(key, [...children.get(key) ?? [], row]);
171
+ }
172
+ const groups = [];
173
+ for (const [key, head] of heads)
174
+ groups.push([
175
+ ...withLeaves(head, 0),
176
+ ...(children.get(key) ?? []).flatMap((row) => withLeaves(row, 1))
177
+ ]);
178
+ for (const [key, kids] of children) {
179
+ if (heads.has(key)) continue;
180
+ const parent = known.get(key);
181
+ groups.push([
182
+ {
183
+ kind: "fence",
184
+ key,
185
+ summary: kids[0].parent?.summary ?? "",
186
+ ...parent ? { parent } : {}
187
+ },
188
+ ...kids.flatMap((row) => withLeaves(row, 1))
189
+ ]);
190
+ }
191
+ if (orphans.length)
192
+ groups.push([
193
+ ...groups.length ? [{ kind: "fence", key: null, summary: "No epic" }] : [],
194
+ ...orphans.flatMap((row) => withLeaves(row, 0))
195
+ ]);
196
+ return groups.flatMap(
197
+ (group, i) => i === 0 ? group : [{ kind: "gap" }, ...group]
198
+ );
199
+ };
200
+ var firstName = (assignee) => assignee === "unassigned" ? "unassigned" : `@${assignee.split(" ")[0]}`;
201
+ var fenceNote = (parent, tabs, { viewer, mine }) => {
202
+ if (!parent) return { segment: "", behind: false };
203
+ const theirs = parent.assignee !== viewer;
204
+ if (!mine || theirs)
205
+ return { segment: firstName(parent.assignee), behind: false };
206
+ const own = tabOf(parent, tabs);
207
+ return {
208
+ segment: tabs.tabs.find((t) => t.value === own)?.label ?? "",
209
+ behind: parent.behind !== void 0
210
+ };
211
+ };
212
+ var blockIndexOfStop = (blocks, stopIndex) => {
213
+ let seen = -1;
214
+ for (let i = 0; i < blocks.length; i++) {
215
+ if (isStop(blocks[i]) && ++seen === stopIndex) return i;
216
+ }
217
+ return 0;
218
+ };
219
+ var isLastAtDepth = (blocks, i) => {
220
+ const depth = blocks[i].depth;
221
+ for (let j = i + 1; j < blocks.length; j++) {
222
+ const b = blocks[j];
223
+ if (!isStop(b) || b.depth < depth) return true;
224
+ if (b.depth === depth) return false;
225
+ }
226
+ return true;
227
+ };
228
+ var treePrefix = (blocks, i) => {
229
+ const block = blocks[i];
230
+ if (!block || !isStop(block) || block.depth === 0) return "";
231
+ const own = isLastAtDepth(blocks, i) ? "\u2514\u2500 " : "\u251C\u2500 ";
232
+ if (block.depth === 1) return own;
233
+ let parent = i - 1;
234
+ while (parent >= 0 && blocks[parent].depth !== 1) parent -= 1;
235
+ const stem = parent >= 0 && !isLastAtDepth(blocks, parent) ? "\u2502 " : " ";
236
+ return stem + own;
237
+ };
238
+ var subtreeOf = (blocks, i) => {
239
+ const head = blocks[i];
240
+ if (!head || head.kind !== "issue") return head ? [head] : [];
241
+ const out = [head];
242
+ for (let j = i + 1; j < blocks.length; j++) {
243
+ const b = blocks[j];
244
+ if (!isStop(b) || b.depth <= head.depth) break;
245
+ out.push(b);
246
+ }
247
+ return out;
248
+ };
249
+ var priorityGlyph = (name) => {
250
+ const n = (name ?? "").trim().toLowerCase();
251
+ if (/^(highest|blocker|critical|p[01])$/.test(n)) return "\u21C8";
252
+ if (/^(high|urgent|major|p2)$/.test(n)) return "\u2191";
253
+ if (/^(medium|normal|p3)$/.test(n)) return "=";
254
+ if (/^(low|minor|p4)$/.test(n)) return "\u2193";
255
+ if (/^(lowest|trivial|p5)$/.test(n)) return "\u21CA";
256
+ return " ";
257
+ };
258
+ var UNITS = [
259
+ [60, "m"],
260
+ [60, "h"],
261
+ [24, "d"],
262
+ [7, "w"]
263
+ ];
264
+ var relativeAge = (iso, now = Date.now()) => {
265
+ const then = Date.parse(iso);
266
+ if (Number.isNaN(then)) return "";
267
+ let value = Math.max(0, Math.round((now - then) / 1e3));
268
+ let unit = "s";
269
+ for (const [size, next] of UNITS) {
270
+ if (value < size) break;
271
+ value = Math.round(value / size);
272
+ unit = next;
273
+ }
274
+ return `${value}${unit}`;
275
+ };
276
+ var pillVariantFor = (row) => {
277
+ if (row.container) return "group";
278
+ const t = row.type.toLowerCase();
279
+ if (t === "bug") return "error";
280
+ if (t === "story") return "info";
281
+ return "muted";
282
+ };
283
+ var tabLabelOf = (to, tabs) => {
284
+ const value = tabOf(
285
+ {
286
+ status: to.name,
287
+ statusId: to.id ?? "",
288
+ category: to.category ?? "unknown"
289
+ },
290
+ tabs
291
+ );
292
+ return tabs.tabs.find((t) => t.value === value)?.label ?? to.name;
293
+ };
294
+ var mapRow = (model, key, f) => ({
295
+ ...model,
296
+ rows: model.rows.map((row) => row.key === key ? f(row) : row)
297
+ });
298
+ var pendingRow = (model, key, to) => mapRow(model, key, (row) => ({
299
+ ...row,
300
+ pending: tabLabelOf(to, model.tabs)
301
+ }));
302
+ var transitionRow = (model, key, to) => mapRow(model, key, ({ pending: _, ...row }) => ({
303
+ ...row,
304
+ status: to.name,
305
+ statusId: to.id ?? "",
306
+ category: to.category ?? "unknown"
307
+ }));
308
+ var settleRow = (model, key) => mapRow(model, key, ({ pending: _, ...row }) => row);
309
+ var parentsOf = async (client, rows) => {
310
+ const held = new Set(rows.map((r) => r.key));
311
+ const missing = [
312
+ ...new Set(
313
+ rows.map((r) => r.parent?.key).filter((key) => key !== void 0 && !held.has(key))
314
+ )
315
+ ];
316
+ if (missing.length === 0) return [];
317
+ const issues = await client.searchIssues(
318
+ `key in (${missing.map(jqlEscape).join(", ")})`,
319
+ { limit: missing.length }
320
+ );
321
+ return issues.map(toBoardRow);
322
+ };
323
+ var boardOf = async (client, jql, { tabs, board, limit = 200 } = {}) => {
324
+ const [issues, derived] = await Promise.all([
325
+ client.searchIssues(jql, { limit }),
326
+ tabs?.length ? Promise.resolve(tabsFromConfig(tabs)) : board !== void 0 ? client.getBoardConfiguration(board).then(tabsFromBoard) : Promise.resolve(null)
327
+ ]);
328
+ return { rows: issues.map(toBoardRow), tabs: derived ?? tabsFromCategories() };
329
+ };
330
+ var COLOUR = {
331
+ "\u21C8": colors.warning,
332
+ "\u2191": colors.warning,
333
+ "=": colors.muted,
334
+ "\u2193": colors.info,
335
+ "\u21CA": colors.info,
336
+ " ": void 0
337
+ };
338
+ var priorityMarker = (name) => {
339
+ const marker = priorityGlyph(name);
340
+ const color = COLOUR[marker];
341
+ return color ? { marker, color } : { marker };
342
+ };
10
343
  var truncate = (s, max) => s.length <= max ? s : `${s.slice(0, Math.max(0, max - 1))}\u2026`;
11
344
  var originOf = (a) => a.origins.map(
12
345
  (o) => o.kind === "comment" ? `comment by ${o.author ?? "unknown"}` : o.kind === "description" ? "description" : "issue"
@@ -20,14 +353,52 @@ var humanSize = (bytes) => {
20
353
  const value = bytes / 1024 ** e;
21
354
  return `${e === 0 ? value : value.toFixed(1)}${units[e]}`;
22
355
  };
23
- var commentsAsMarkdown = (issue) => issue.comments.length === 0 ? "_No comments._" : issue.comments.map((c) => `**${c.author}** \xB7 ${c.created}
356
+ var ago = (iso) => {
357
+ const rel = relativeAge(iso);
358
+ return rel ? `${rel} ago` : iso;
359
+ };
360
+ var documentOf = (issue) => {
361
+ const parts = [issue.description || "_No description._"];
362
+ if (issue.comments.length > 0)
363
+ parts.push(
364
+ `## Comments (${issue.comments.length})`,
365
+ ...issue.comments.map(
366
+ (c) => `**${c.author}** \xB7 ${ago(c.created)}
24
367
 
25
- ${c.body}`).join("\n\n---\n\n");
26
- var attachmentsAsMarkdown = (issue) => issue.attachments.length === 0 ? "_No attachments._" : issue.attachments.map(
27
- (a) => `- \`${a.filename}\` \u2014 ${a.mimeType}, ${humanSize(a.size)}
28
- from ${originOf(a)}
368
+ ${c.body}`
369
+ )
370
+ );
371
+ if (issue.attachments.length > 0)
372
+ parts.push(
373
+ `## Attachments (${issue.attachments.length})`,
374
+ issue.attachments.map(
375
+ (a) => `- \`${a.filename}\` \u2014 ${a.mimeType}, ${humanSize(a.size)} \xB7 from ${originOf(a)}
29
376
  \`jira attachment read ${issue.key} ${a.filename}\``
30
- ).join("\n");
377
+ ).join("\n")
378
+ );
379
+ return parts.join("\n\n");
380
+ };
381
+ var FactsLine = ({ issue }) => {
382
+ const priority = priorityMarker(issue.priority ?? null);
383
+ const lead = priority.marker !== " " && priority.marker !== "=" ? /* @__PURE__ */ jsxs(Fragment, { children: [
384
+ /* @__PURE__ */ jsx(Text, { color: priority.color, children: priority.marker }),
385
+ /* @__PURE__ */ jsxs(Text, { children: [
386
+ " ",
387
+ issue.priority
388
+ ] })
389
+ ] }) : null;
390
+ const plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
391
+ const facts = [
392
+ `by ${issue.reporter}`,
393
+ ...issue.labels.length > 0 ? [`labels ${issue.labels.join(", ")}`] : [],
394
+ plural(issue.comments.length, "comment"),
395
+ ...issue.attachments.length > 0 ? [plural(issue.attachments.length, "attachment")] : []
396
+ ];
397
+ return /* @__PURE__ */ jsxs(Box, { children: [
398
+ lead,
399
+ /* @__PURE__ */ jsx(Text, { color: colors.muted, children: (lead ? " \xB7 " : "") + facts.join(" \xB7 ") })
400
+ ] });
401
+ };
31
402
  var IssueDetailView = ({
32
403
  issue,
33
404
  height,
@@ -40,16 +411,6 @@ var IssueDetailView = ({
40
411
  frame,
41
412
  theme
42
413
  }) => {
43
- const tabs = [
44
- { value: "description", label: "Description" },
45
- { value: "comments", label: "Comments", count: issue.comments.length },
46
- {
47
- value: "attachments",
48
- label: "Attachments",
49
- count: issue.attachments.length
50
- }
51
- ];
52
- const { active } = useTabs(tabs, { initial: "description" });
53
414
  const [scroll, setScroll] = useState(0);
54
415
  useInput((input, key) => {
55
416
  if (input === "t" && onTransition) return onTransition();
@@ -59,12 +420,11 @@ var IssueDetailView = ({
59
420
  if (key.downArrow || input === "j") setScroll((s) => s + 1);
60
421
  if (key.upArrow || input === "k") setScroll((s) => Math.max(0, s - 1));
61
422
  });
62
- const source = active === "comments" ? commentsAsMarkdown(issue) : active === "attachments" ? attachmentsAsMarkdown(issue) : issue.description || "_No description._";
63
- const keyValueRows = 3 + (issue.parent ? 1 : 0) + (issue.labels.length > 0 ? 1 : 0) + (frame ? 0 : 1);
64
- const chromeLines = keyValueRows + 1 + 2 + 1 + (frame ? 0 : 4);
423
+ const source = documentOf(issue);
424
+ const keyValueRows = 2 + (issue.parent ? 1 : 0) + (frame ? 0 : 1);
425
+ const chromeLines = keyValueRows + 1 + 1 + (frame ? 0 : 4);
65
426
  const bodyHeight = Math.max(3, height - chromeLines);
66
427
  const hints = [
67
- ["\u2190\u2192", "section"],
68
428
  ["\u2191\u2193", "scroll"],
69
429
  ...onTransition ? [["t", "move"]] : [],
70
430
  ...onComment ? [["c", "comment"]] : [],
@@ -74,20 +434,27 @@ var IssueDetailView = ({
74
434
  ];
75
435
  const body = /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
76
436
  /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
437
+ /* @__PURE__ */ jsx(KeyValue, { label: "status", value: /* @__PURE__ */ jsx(Badge, { children: issue.status }) }),
438
+ frame ? null : /* @__PURE__ */ jsx(KeyValue, { label: "type", value: issue.type }),
439
+ /* @__PURE__ */ jsx(
440
+ KeyValue,
441
+ {
442
+ label: "assignee",
443
+ value: issue.assignee === "unassigned" ? /* @__PURE__ */ jsx(Text, { color: colors.muted, children: "unassigned" }) : issue.assignee
444
+ }
445
+ ),
77
446
  issue.parent && /* @__PURE__ */ jsx(
78
447
  KeyValue,
79
448
  {
80
449
  label: "parent",
81
- value: `${issue.parent.key} ${truncate(issue.parent.summary, 60)}`
450
+ value: /* @__PURE__ */ jsxs(Fragment, { children: [
451
+ /* @__PURE__ */ jsx(Text, { color: colors.secondary, children: issue.parent.key }),
452
+ /* @__PURE__ */ jsx(Text, { children: ` ${truncate(issue.parent.summary, 60)}` })
453
+ ] })
82
454
  }
83
455
  ),
84
- /* @__PURE__ */ jsx(KeyValue, { label: "status", value: /* @__PURE__ */ jsx(Badge, { children: issue.status }) }),
85
- frame ? null : /* @__PURE__ */ jsx(KeyValue, { label: "type", value: issue.type }),
86
- /* @__PURE__ */ jsx(KeyValue, { label: "assignee", value: issue.assignee }),
87
- /* @__PURE__ */ jsx(KeyValue, { label: "reporter", value: issue.reporter }),
88
- issue.labels.length > 0 && /* @__PURE__ */ jsx(KeyValue, { label: "labels", value: issue.labels.join(", ") })
456
+ /* @__PURE__ */ jsx(FactsLine, { issue })
89
457
  ] }),
90
- /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Tabs, { items: tabs, active: active ?? "description" }) }),
91
458
  /* @__PURE__ */ jsx(Box, { marginTop: 1, height: bodyHeight, overflow: "hidden", children: /* @__PURE__ */ jsx(
92
459
  MarkdownViewport,
93
460
  {
@@ -129,13 +496,14 @@ var issueDetailOf = async (client, baseUrl, key) => {
129
496
  assignee: f.assignee?.displayName ?? "unassigned",
130
497
  reporter: f.reporter?.displayName ?? "\u2014",
131
498
  labels: f.labels ?? [],
499
+ ...f.priority?.name ? { priority: f.priority.name } : {},
132
500
  parent: f.parent ? { key: f.parent.key, summary: f.parent.fields?.summary ?? "" } : void 0,
133
501
  url: `${baseUrl}/browse/${issue.key}`,
134
502
  description: adfToMarkdown(f.description, filenameOf),
135
503
  comments: (f.comment?.comments ?? []).map((c) => ({
136
504
  id: c.id,
137
505
  author: c.author?.displayName ?? "unknown",
138
- created: c.created?.slice(0, 10) ?? "",
506
+ created: c.created ?? "",
139
507
  body: adfToMarkdown(c.body, filenameOf)
140
508
  })),
141
509
  attachments
@@ -144,7 +512,11 @@ var issueDetailOf = async (client, baseUrl, key) => {
144
512
  var transitionsOf = async (client, key) => (await client.getTransitions(key)).transitions.map((t) => ({
145
513
  id: t.id,
146
514
  name: t.name,
147
- to: t.to?.name ?? t.name
515
+ to: {
516
+ ...t.to?.id ? { id: t.to.id } : {},
517
+ name: t.to?.name ?? t.name,
518
+ ...t.to?.statusCategory?.key ? { category: categoryOf(t.to.statusCategory.key) } : {}
519
+ }
148
520
  }));
149
521
  var useTermSize = () => {
150
522
  const { stdout } = useStdout();
@@ -169,6 +541,7 @@ var IssueBody = ({
169
541
  client,
170
542
  baseUrl,
171
543
  issueKey,
544
+ summary,
172
545
  onExit,
173
546
  width,
174
547
  height,
@@ -192,7 +565,7 @@ var IssueBody = ({
192
565
  }, [client, baseUrl, issueKey]);
193
566
  const framed = (body) => frame ? frame({
194
567
  title: issueKey,
195
- subtitle: "",
568
+ subtitle: summary ?? "",
196
569
  hints: [["\u232B", "back"]],
197
570
  body
198
571
  }) : body;
@@ -219,192 +592,14 @@ var IssueBody = ({
219
592
  }
220
593
  );
221
594
  };
222
- var CATEGORIES = ["new", "indeterminate", "done"];
223
- var categoryOf = (key) => CATEGORIES.find((c) => c === key) ?? "unknown";
224
- var toBoardRow = (issue) => ({
225
- key: issue.key,
226
- summary: issue.fields.summary ?? "",
227
- status: issue.fields.status?.name ?? "\u2014",
228
- statusId: issue.fields.status?.id ?? "",
229
- category: categoryOf(issue.fields.status?.statusCategory?.key),
230
- type: issue.fields.issuetype?.name ?? "",
231
- container: isContainerType(issue.fields.issuetype),
232
- priority: issue.fields.priority?.name ?? null,
233
- ...issue.fields.parent ? {
234
- parent: {
235
- key: issue.fields.parent.key,
236
- summary: issue.fields.parent.fields?.summary ?? ""
237
- }
238
- } : {},
239
- assignee: issue.fields.assignee?.displayName ?? "unassigned",
240
- updated: issue.fields.updated ?? ""
241
- });
242
- var OFF_BOARD = "__off_board__";
243
- var CATEGORY_TABS = [
244
- { value: "todo", label: "To do", category: "new" },
245
- { value: "doing", label: "In progress", category: "indeterminate" },
246
- { value: "done", label: "Done", category: "done" }
247
- ];
248
- var withOffBoard = (tabs) => [
249
- ...tabs,
250
- { value: OFF_BOARD, label: "Off board", offBoard: true }
251
- ];
252
- var tabsFromConfig = (configured) => ({
253
- source: "config",
254
- tabs: withOffBoard(
255
- configured.map((t, i) => ({
256
- value: `config:${i}`,
257
- label: t.label,
258
- statuses: new Set(t.statuses.map((s) => s.toLowerCase()))
259
- }))
260
- )
261
- });
262
- var tabsFromBoard = (configuration) => {
263
- const columns = configuration.columnConfig?.columns ?? [];
264
- if (columns.length === 0) return null;
265
- return {
266
- source: "board",
267
- tabs: withOffBoard(
268
- columns.map((c, i) => ({
269
- value: `column:${i}`,
270
- label: c.name,
271
- statuses: new Set(c.statuses.map((s) => s.id))
272
- }))
273
- )
274
- };
275
- };
276
- var tabsFromCategories = () => ({
277
- source: "categories",
278
- tabs: CATEGORY_TABS
279
- });
280
- var tabOf = (row, { tabs }) => {
281
- const claimed = tabs.find(
282
- (t) => t.statuses?.has(row.statusId) || t.statuses?.has(row.status.toLowerCase())
283
- );
284
- if (claimed) return claimed.value;
285
- const byCategory = tabs.find((t) => t.category && t.category === row.category);
286
- if (byCategory) return byCategory.value;
287
- return tabs.find((t) => t.offBoard)?.value ?? "doing";
288
- };
289
- var countsFor = (rows, tabs) => {
290
- const counts = Object.fromEntries(
291
- tabs.tabs.map((t) => [t.value, 0])
292
- );
293
- for (const row of rows)
294
- counts[tabOf(row, tabs)] = (counts[tabOf(row, tabs)] ?? 0) + 1;
295
- return counts;
296
- };
297
- var visibleTabs = (tabs, counts) => tabs.tabs.filter((t) => !t.offBoard || (counts[t.value] ?? 0) > 0);
298
- var blocksFor = (rows) => {
299
- const hasStructure = rows.some((r) => r.parent || r.container);
300
- if (!hasStructure)
301
- return rows.map((row) => ({ kind: "issue", row, depth: 0 }));
302
- const heads = new Map(rows.filter((r) => r.container).map((r) => [r.key, r]));
303
- const children = /* @__PURE__ */ new Map();
304
- const orphans = [];
305
- for (const row of rows) {
306
- if (row.container) continue;
307
- const key = row.parent?.key;
308
- if (!key) {
309
- orphans.push(row);
310
- continue;
311
- }
312
- children.set(key, [...children.get(key) ?? [], row]);
313
- }
314
- const groups = [];
315
- for (const [key, head] of heads)
316
- groups.push([
317
- { kind: "issue", row: head, depth: 0 },
318
- ...(children.get(key) ?? []).map((row) => ({
319
- kind: "issue",
320
- row,
321
- depth: 1
322
- }))
323
- ]);
324
- for (const [key, kids] of children) {
325
- if (heads.has(key)) continue;
326
- groups.push([
327
- { kind: "fence", key, summary: kids[0].parent?.summary ?? "" },
328
- ...kids.map((row) => ({ kind: "issue", row, depth: 1 }))
329
- ]);
330
- }
331
- if (orphans.length)
332
- groups.push([
333
- ...groups.length ? [{ kind: "fence", key: null, summary: "No epic" }] : [],
334
- ...orphans.map((row) => ({ kind: "issue", row, depth: 0 }))
335
- ]);
336
- return groups.flatMap(
337
- (group, i) => i === 0 ? group : [{ kind: "gap" }, ...group]
338
- );
339
- };
340
- var blockIndexOfIssue = (blocks, issueIndex) => {
341
- let seen = -1;
342
- for (let i = 0; i < blocks.length; i++) {
343
- if (blocks[i].kind === "issue" && ++seen === issueIndex) return i;
344
- }
345
- return 0;
346
- };
347
- var priorityGlyph = (name) => {
348
- const n = (name ?? "").trim().toLowerCase();
349
- if (/^(highest|blocker|critical|p[01])$/.test(n)) return "\u21C8";
350
- if (/^(high|urgent|major|p2)$/.test(n)) return "\u2191";
351
- if (/^(medium|normal|p3)$/.test(n)) return "=";
352
- if (/^(low|minor|p4)$/.test(n)) return "\u2193";
353
- if (/^(lowest|trivial|p5)$/.test(n)) return "\u21CA";
354
- return " ";
355
- };
356
- var UNITS = [
357
- [60, "m"],
358
- [60, "h"],
359
- [24, "d"],
360
- [7, "w"]
361
- ];
362
- var relativeAge = (iso, now = Date.now()) => {
363
- const then = Date.parse(iso);
364
- if (Number.isNaN(then)) return "";
365
- let value = Math.max(0, Math.round((now - then) / 1e3));
366
- let unit = "s";
367
- for (const [size, next] of UNITS) {
368
- if (value < size) break;
369
- value = Math.round(value / size);
370
- unit = next;
371
- }
372
- return `${value}${unit}`;
373
- };
374
- var pillVariantFor = (row) => {
375
- if (row.container) return "group";
376
- const t = row.type.toLowerCase();
377
- if (t === "bug") return "error";
378
- if (t === "story") return "info";
379
- return "muted";
380
- };
381
- var boardOf = async (client, jql, { tabs, board, limit = 200 } = {}) => {
382
- const [issues, derived] = await Promise.all([
383
- client.searchIssues(jql, { limit }),
384
- tabs?.length ? Promise.resolve(tabsFromConfig(tabs)) : board !== void 0 ? client.getBoardConfiguration(board).then(tabsFromBoard) : Promise.resolve(null)
385
- ]);
386
- return { rows: issues.map(toBoardRow), tabs: derived ?? tabsFromCategories() };
387
- };
388
- var COLOUR = {
389
- "\u21C8": colors.warning,
390
- "\u2191": colors.warning,
391
- "=": colors.muted,
392
- "\u2193": colors.info,
393
- "\u21CA": colors.info,
394
- " ": void 0
395
- };
396
- var priorityMarker = (name) => {
397
- const marker = priorityGlyph(name);
398
- const color = COLOUR[marker];
399
- return color ? { marker, color } : { marker };
400
- };
401
595
  var CHROME = 6;
596
+ var ROW_CHROME = 7;
402
597
  var LEGEND = [
403
598
  ["\u21C8 \u2191", "priority above the default"],
404
599
  ["=", "default priority"],
405
600
  ["\u2193 \u21CA", "priority below the default"],
406
601
  ["\u2500\u2500 epic \u2500\u2500", "a parent not in this tab; its rows hang beneath"],
407
- ["\u2514\u2500", "a row under the container above it"],
602
+ ["\u251C\u2500 \u2514\u2500", "a row under the container above it; \u2514\u2500 is the last"],
408
603
  ["story \xB7 bug \xB7 task", "issue type"],
409
604
  ["2d", "time since last update"]
410
605
  ];
@@ -424,8 +619,21 @@ var tabLegendLine = (model) => model.tabs.source === "categories" ? [
424
619
  "To do \xB7 In progress \xB7 Done",
425
620
  "status category \u2014 not any one board's column"
426
621
  ] : model.tabs.source === "board" ? ["tabs", "this board's own columns, matched by status id"] : ["tabs", "your config file's tabs"];
622
+ var RowNote = ({ row, note }) => {
623
+ if (row.pending)
624
+ return /* @__PURE__ */ jsx(Box, { flexShrink: 0, paddingLeft: 1, children: /* @__PURE__ */ jsx(Text, { color: colors.accent, children: `\u22EF \u2192 ${row.pending}` }) });
625
+ const behind = row.behind !== void 0;
626
+ const dim = [row.behind || "", note ?? ""].filter(Boolean).join(" \xB7 ");
627
+ if (!behind && !dim) return null;
628
+ return /* @__PURE__ */ jsxs(Box, { flexShrink: 0, paddingLeft: 1, children: [
629
+ behind ? /* @__PURE__ */ jsx(Text, { color: colors.warning, bold: true, children: "behind" }) : null,
630
+ dim ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: (behind ? " \xB7 " : "") + dim }) : null
631
+ ] });
632
+ };
427
633
  var IssueBoard = ({
428
634
  model,
635
+ leaves,
636
+ parents,
429
637
  viewer,
430
638
  loadedAt,
431
639
  scope,
@@ -463,7 +671,11 @@ var IssueBoard = ({
463
671
  (r) => `${r.key} ${r.summary}`.toLowerCase().includes(words)
464
672
  );
465
673
  }, [model.rows, search]);
466
- const counts = countsFor(narrowed, model.tabs);
674
+ const placement = useMemo(
675
+ () => placementOf(narrowed, model.tabs),
676
+ [narrowed, model.tabs]
677
+ );
678
+ const counts = countsFor(narrowed, model.tabs, placement);
467
679
  const shown = visibleTabs(model.tabs, counts);
468
680
  const tabItems = shown.map((t) => ({
469
681
  value: t.value,
@@ -477,18 +689,26 @@ var IssueBoard = ({
477
689
  isActive: listFocused
478
690
  });
479
691
  const tab = active ?? initial ?? "";
692
+ const under = leaves?.under;
480
693
  const blocks = useMemo(
481
- () => blocksFor(narrowed.filter((r) => tabOf(r, model.tabs) === tab)),
482
- [narrowed, model.tabs, tab]
483
- );
484
- const issues = blocks.filter(
485
- (b) => b.kind === "issue"
694
+ () => blocksFor(
695
+ rowsInTab(narrowed, model.tabs, tab, placement),
696
+ under,
697
+ parents
698
+ ),
699
+ [narrowed, model.tabs, tab, placement, under, parents]
486
700
  );
487
- const { cursor, setCursor } = useListCursor(issues.length, {
701
+ const stops = stopsOf(blocks);
702
+ const { cursor, setCursor } = useListCursor(stops.length, {
488
703
  vimKeys: true,
489
704
  isActive: listFocused
490
705
  });
491
706
  useEffect(() => setCursor(0), [tab, setCursor]);
707
+ const openStop = (stop) => {
708
+ if (!stop) return;
709
+ if (stop.kind === "issue") onOpen(stop.row.key);
710
+ else leaves?.onOpen(stop.data);
711
+ };
492
712
  useInput((input, key) => {
493
713
  if (search.open) {
494
714
  if (key.escape) setSearch((s) => ({ ...s, open: false }));
@@ -503,7 +723,7 @@ var IssueBoard = ({
503
723
  if (input === "?" || key.escape) setLegend(false);
504
724
  return;
505
725
  }
506
- if (key.return && issues[cursor]) onOpen(issues[cursor].row.key);
726
+ if (key.return) openStop(stops[cursor]);
507
727
  if (input === "/")
508
728
  setSearch((s) => ({
509
729
  ...s,
@@ -518,7 +738,7 @@ var IssueBoard = ({
518
738
  const searching = search.open || scope.kind === "search";
519
739
  const chrome = CHROME + (searching ? 1 : 0) + (searchError ? 1 : 0);
520
740
  const size = Math.max(3, height - chrome);
521
- const focus = blockIndexOfIssue(blocks, cursor);
741
+ const focus = blockIndexOfStop(blocks, cursor);
522
742
  const { start, end } = windowFor(focus, blocks.length, size);
523
743
  const keyWidth = Math.max(8, ...narrowed.map((r) => r.key.length));
524
744
  const typeWidth = Math.max(
@@ -589,20 +809,61 @@ var IssueBoard = ({
589
809
  /* @__PURE__ */ jsx(Box, { flexDirection: "column", marginTop: 1, height: size, paddingRight: 1, children: legend ? [...LEGEND, tabLegendLine(model)].map(([glyph, meaning]) => /* @__PURE__ */ jsxs(Box, { paddingLeft: 4, children: [
590
810
  /* @__PURE__ */ jsx(Box, { width: 28, flexShrink: 0, children: /* @__PURE__ */ jsx(Text, { color: colors.accent, children: glyph }) }),
591
811
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: meaning })
592
- ] }, glyph)) : issues.length === 0 ? /* @__PURE__ */ jsx(Box, { paddingLeft: 4, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: emptyHint() }) }) : blocks.slice(start, end).map(
593
- (block, i) => block.kind === "gap" ? /* @__PURE__ */ jsx(Text, { children: " " }, `gap:${start + i}`) : block.kind === "fence" ? /* @__PURE__ */ jsx(Box, { paddingLeft: 4, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: rule(
594
- block.key ? `${block.summary} \xB7 ${block.key}` : block.summary,
595
- width - 7
596
- ) }) }, `f:${block.key ?? "none"}`) : /* @__PURE__ */ jsxs(SelectableRow, { active: start + i === focus, children: [
597
- block.depth === 1 ? /* @__PURE__ */ jsx(Box, { flexShrink: 0, width: 3, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2514\u2500 " }) }) : null,
812
+ ] }, glyph)) : stops.length === 0 ? /* @__PURE__ */ jsx(Box, { paddingLeft: 4, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: emptyHint() }) }) : blocks.slice(start, end).map((block, i) => {
813
+ const at = start + i;
814
+ const focused = at === focus;
815
+ const prefix = treePrefix(blocks, at);
816
+ return block.kind === "gap" ? /* @__PURE__ */ jsx(Text, { children: " " }, `gap:${at}`) : block.kind === "fence" ? (() => {
817
+ const { segment, behind } = fenceNote(
818
+ block.parent,
819
+ model.tabs,
820
+ {
821
+ viewer,
822
+ mine: scope.kind === "mine"
823
+ }
824
+ );
825
+ const tail = [behind ? "behind" : "", segment].filter(Boolean).join(" \xB7 ");
826
+ const label = block.key ? `${block.summary} \xB7 ${block.key}` : block.summary;
827
+ return /* @__PURE__ */ jsxs(Box, { paddingLeft: 4, children: [
828
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: rule(
829
+ label,
830
+ width - ROW_CHROME - (tail ? tail.length + 1 : 0)
831
+ ) }),
832
+ tail ? /* @__PURE__ */ jsxs(Box, { flexShrink: 0, paddingLeft: 1, children: [
833
+ behind ? /* @__PURE__ */ jsx(Text, { color: colors.warning, bold: true, children: "behind" }) : null,
834
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: (behind ? " \xB7 " : "") + segment })
835
+ ] }) : null
836
+ ] }, `f:${block.key ?? "none"}`);
837
+ })() : block.kind === "leaf" ? /* @__PURE__ */ jsxs(
838
+ SelectableRow,
839
+ {
840
+ active: focused,
841
+ children: [
842
+ /* @__PURE__ */ jsx(Box, { flexShrink: 0, width: prefix.length, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: prefix }) }),
843
+ leaves?.render(block.data, {
844
+ active: focused,
845
+ width: width - ROW_CHROME - prefix.length
846
+ })
847
+ ]
848
+ },
849
+ `leaf:${block.parent.key}:${leaves?.keyOf(block.data)}`
850
+ ) : /* @__PURE__ */ jsxs(SelectableRow, { active: focused, children: [
851
+ prefix ? /* @__PURE__ */ jsx(Box, { flexShrink: 0, width: prefix.length, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: prefix }) }) : null,
598
852
  /* @__PURE__ */ jsx(Box, { flexShrink: 0, width: 2, children: /* @__PURE__ */ jsx(Text, { color: priorityMarker(block.row.priority).color, children: priorityMarker(block.row.priority).marker }) }),
599
- /* @__PURE__ */ jsx(Box, { flexShrink: 0, width: keyWidth + 2, children: /* @__PURE__ */ jsx(Text, { color: colors.accent, bold: start + i === focus, children: block.row.key }) }),
853
+ /* @__PURE__ */ jsx(Box, { flexShrink: 0, width: keyWidth + 2, children: /* @__PURE__ */ jsx(Text, { color: colors.accent, bold: focused, children: block.row.key }) }),
600
854
  /* @__PURE__ */ jsx(Box, { flexShrink: 0, width: typeWidth + 2, children: /* @__PURE__ */ jsx(Pill, { tone: "soft", variant: pillVariantFor(block.row), children: block.row.type.toLowerCase() }) }),
601
- /* @__PURE__ */ jsx(Box, { flexGrow: 1, children: /* @__PURE__ */ jsx(Text, { wrap: "truncate-end", bold: start + i === focus, children: block.row.summary }) }),
855
+ /* @__PURE__ */ jsx(Box, { flexGrow: 1, children: /* @__PURE__ */ jsx(Text, { wrap: "truncate-end", bold: focused, children: block.row.summary }) }),
856
+ /* @__PURE__ */ jsx(
857
+ RowNote,
858
+ {
859
+ row: block.row,
860
+ note: headNote(block.row, model.tabs, tab, placement)
861
+ }
862
+ ),
602
863
  /* @__PURE__ */ jsx(Box, { flexShrink: 0, width: 5, justifyContent: "flex-end", children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: relativeAge(block.row.updated, now) }) })
603
- ] }, block.row.key)
604
- ) }),
605
- /* @__PURE__ */ jsx(Box, { marginTop: 1, paddingLeft: 2, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: issues.length ? `${cursor + 1}/${issues.length}` : " " }) })
864
+ ] }, block.row.key);
865
+ }) }),
866
+ /* @__PURE__ */ jsx(Box, { marginTop: 1, paddingLeft: 2, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: stops.length ? `${cursor + 1}/${stops.length}` : " " }) })
606
867
  ] });
607
868
  if (frame) return frame({ facts, title, hints, body });
608
869
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
@@ -711,4 +972,4 @@ var mockBoard = () => ({
711
972
  tabs: tabsFromCategories()
712
973
  });
713
974
 
714
- export { IssueBoard, IssueBody, IssueDetailView, MOCK_BOARD_ROWS, OFF_BOARD, blockIndexOfIssue, blocksFor, boardOf, countsFor, issueDetailOf, mockBoard, pillVariantFor, priorityGlyph, priorityMarker, relativeAge, tabOf, tabsFromBoard, tabsFromCategories, tabsFromConfig, toBoardRow, transitionsOf, visibleTabs };
975
+ export { IssueBoard, IssueBody, IssueDetailView, MOCK_BOARD_ROWS, OFF_BOARD, blockIndexOfStop, blocksFor, boardOf, countsFor, fenceNote, headNote, isStop, issueDetailOf, mockBoard, parentsOf, pendingRow, pillVariantFor, placementOf, priorityGlyph, priorityMarker, relativeAge, rowsInTab, settleRow, stopsOf, subtreeOf, tabLabelOf, tabOf, tabsFromBoard, tabsFromCategories, tabsFromConfig, toBoardRow, transitionRow, transitionsOf, treePrefix, visibleTabs, withBehind };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kud/jira-ink",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
4
4
  "description": "Ink components for viewing Jira issues: the board and the issue detail screen, shared by jira-cli's TUI and cockpit",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",