@kud/jira-ink 0.8.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 -112
  2. package/dist/index.js +265 -40
  3. package/package.json +1 -1
package/dist/index.d.ts CHANGED
@@ -2,110 +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
- /** Jira's priority name, verbatim; absent when the field is off. */
23
- priority?: string;
24
- parent?: {
25
- key: string;
26
- summary: string;
27
- };
28
- url: string;
29
- description: string;
30
- comments: {
31
- id: string;
32
- author: string;
33
- created: string;
34
- body: string;
35
- }[];
36
- attachments: LocatedAttachment[];
37
- };
38
- type Transition = {
39
- id: string;
40
- name: string;
41
- to: string;
42
- };
43
- declare const issueDetailOf: (client: JiraClient, baseUrl: string, key: string) => Promise<IssueDetail>;
44
- declare const transitionsOf: (client: JiraClient, key: string) => Promise<Transition[]>;
45
-
46
- type IssueDetailViewProps = {
47
- issue: IssueDetail;
48
- height: number;
49
- width: number;
50
- onBack: () => void;
51
- /**
52
- * The write actions are optional, and a key whose handler is absent is
53
- * neither bound nor hinted. A host that only READS — cockpit's ticket drill —
54
- * mounts the same screen jira-cli's TUI does, minus the verbs it has no way
55
- * to honour, rather than a hint row promising `t` and `c` that do nothing.
56
- */
57
- onTransition?: () => void;
58
- onComment?: () => void;
59
- onAssign?: () => void;
60
- onOpenBrowser?: () => void;
61
- /**
62
- * A host that draws its own chrome — a bordered drill frame with the title
63
- * on top and the hints pinned at the foot — takes the screen in parts and
64
- * places them. The view then draws neither its title line nor its footer,
65
- * so nothing appears twice. `height` is still the whole area the host has
66
- * given the body, frame excluded.
67
- */
68
- frame?: (parts: {
69
- title: string;
70
- subtitle: string;
71
- hints: Hint[];
72
- body: ReactNode;
73
- }) => ReactNode;
74
- /** Colours for the Markdown body; a host passes its own tokens so links, code and headings sit in its palette. */
75
- theme?: MarkdownTheme;
76
- };
77
- 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;
78
-
79
- type IssueBodyProps = {
80
- client: JiraClient;
81
- /** Instance URL, as `loadConfig` resolves it; only used to build the browser link. */
82
- baseUrl: string;
83
- issueKey: string;
84
- /**
85
- * The summary as the host already knows it — from the list row that was
86
- * opened — so the frame carries the title from the first frame rather than
87
- * only once the fetch answers. The fetched summary wins once it is here.
88
- */
89
- summary?: string;
90
- onExit: () => void;
91
- /** Override the terminal-derived size when the host draws inside a frame. */
92
- width?: number;
93
- height?: number;
94
- /** The host's chrome around the screen — see `IssueDetailView`. */
95
- frame?: IssueDetailViewProps["frame"];
96
- theme?: IssueDetailViewProps["theme"];
97
- };
98
- /**
99
- * The issue screen as a host mounts it: hand it a client and a key, and it
100
- * fetches, draws and reads its own keys until `onExit`. `JenkinsBody`'s shape,
101
- * for the same reason — a cockpit mounting a ticket over its list should not
102
- * have to know how an issue is fetched or what the loading states look like.
103
- *
104
- * Read-only by construction. The write verbs (`t`, `c`, `a`) exist on
105
- * `IssueDetailView` for jira-cli's own TUI, which owns the prompts they need;
106
- * here they are simply not wired, so the hint row does not offer them.
107
- */
108
- 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;
5
+ import { BoardTab as BoardTab$1, JiraClient, JiraBoardConfiguration, JiraIssue, LocatedAttachment, SearchMode } from '@kud/jira';
109
6
 
110
7
  /** Jira's own three, plus `unknown` for a status that arrives without one. */
111
8
  type StatusCategory = "new" | "indeterminate" | "done" | "unknown";
@@ -132,6 +29,22 @@ type BoardRow = {
132
29
  assignee: string;
133
30
  /** ISO timestamp, kept whole so the view can draw a relative age. */
134
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;
135
48
  };
136
49
  declare const toBoardRow: (issue: JiraIssue) => BoardRow;
137
50
  /**
@@ -165,7 +78,44 @@ declare const tabsFromBoard: (configuration: JiraBoardConfiguration) => BoardTab
165
78
  declare const tabsFromCategories: () => BoardTabs;
166
79
  /** The tab a row belongs to. Nothing ever falls out: unclaimed rows go Off board, or In progress on a category board. */
167
80
  declare const tabOf: (row: BoardRow, { tabs }: BoardTabs) => string;
168
- 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>;
169
119
  /** Tabs worth drawing: every configured one, plus Off board only when it holds something. */
170
120
  declare const visibleTabs: (tabs: BoardTabs, counts: Record<string, number>) => BoardTab[];
171
121
  /**
@@ -176,21 +126,85 @@ declare const visibleTabs: (tabs: BoardTabs, counts: Record<string, number>) =>
176
126
  * with no parent — and each group after the first is preceded by a blank
177
127
  * line. A tab where nothing has a parent and nothing is a container degrades
178
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.
179
136
  */
180
- type Block = {
137
+ type Block<L = never> = {
181
138
  kind: "gap";
182
139
  } | {
183
140
  kind: "fence";
184
141
  key: string | null;
185
142
  summary: string;
143
+ parent?: BoardRow;
186
144
  } | {
187
145
  kind: "issue";
188
146
  row: BoardRow;
189
147
  depth: 0 | 1;
148
+ } | {
149
+ kind: "leaf";
150
+ parent: BoardRow;
151
+ depth: 1 | 2;
152
+ data: L;
190
153
  };
191
- declare const blocksFor: (rows: BoardRow[]) => Block[];
192
- /** Index into `blocks` of the n-th issue, so a cursor over issues maps to a line. */
193
- declare const blockIndexOfIssue: (blocks: Block[], issueIndex: number) => number;
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;
189
+ };
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>[];
194
208
  type PriorityGlyph = "⇈" | "↑" | "=" | "↓" | "⇊" | " ";
195
209
  /**
196
210
  * Rank against the scheme's default rung, in Jira's own arrow grammar: two
@@ -216,6 +230,26 @@ type BoardModel = {
216
230
  rows: BoardRow[];
217
231
  tabs: BoardTabs;
218
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;
219
253
  type BoardOptions = {
220
254
  /** Hand-written tabs from the config file — wins over everything. */
221
255
  tabs?: BoardTab$1[];
@@ -223,6 +257,16 @@ type BoardOptions = {
223
257
  board?: number;
224
258
  limit?: number;
225
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[]>;
226
270
  /**
227
271
  * One assignee-scoped query, bucketed client-side. The board supplies the
228
272
  * VOCABULARY, never the population — `getBoardIssues` would drop your
@@ -231,6 +275,119 @@ type BoardOptions = {
231
275
  */
232
276
  declare const boardOf: (client: JiraClient, jql: string, { tabs, board, limit }?: BoardOptions) => Promise<BoardModel>;
233
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
+
234
391
  /** What the board is showing, named in the title row. */
235
392
  type BoardScope = {
236
393
  kind: "mine";
@@ -239,8 +396,31 @@ type BoardScope = {
239
396
  query: string;
240
397
  mode: "jql" | "text";
241
398
  };
242
- 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> = {
243
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[];
244
424
  /** Who the `mine` scope belongs to, for the title row. */
245
425
  viewer: string;
246
426
  loadedAt: number;
@@ -297,7 +477,7 @@ type IssueBoardProps = {
297
477
  * host — only the keys that open and close the layers it pushes itself: the
298
478
  * search input and the legend.
299
479
  */
300
- 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;
301
481
 
302
482
  type PriorityMarker = {
303
483
  /** Always one column wide, so a list filling every row keeps its grid. */
@@ -322,4 +502,4 @@ declare const priorityMarker: (name: string | null) => PriorityMarker;
322
502
  declare const MOCK_BOARD_ROWS: BoardRow[];
323
503
  declare const mockBoard: () => BoardModel;
324
504
 
325
- 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
@@ -2,7 +2,7 @@ import { MarkdownViewport } from '@kud/ink-markdown';
2
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, locateAttachments, adfToMarkdown, looksLikeJql } from '@kud/jira';
5
+ import { isContainerType, jqlEscape, locateAttachments, adfToMarkdown, looksLikeJql } from '@kud/jira';
6
6
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
7
7
  import { exec } from 'child_process';
8
8
 
@@ -74,19 +74,89 @@ var tabOf = (row, { tabs }) => {
74
74
  if (byCategory) return byCategory.value;
75
75
  return tabs.find((t) => t.offBoard)?.value ?? "doing";
76
76
  };
77
- var countsFor = (rows, tabs) => {
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)) => {
78
136
  const counts = Object.fromEntries(
79
137
  tabs.tabs.map((t) => [t.value, 0])
80
138
  );
81
139
  for (const row of rows)
82
- counts[tabOf(row, tabs)] = (counts[tabOf(row, tabs)] ?? 0) + 1;
140
+ for (const tab of placement.tabsOf(row.key))
141
+ counts[tab] = (counts[tab] ?? 0) + 1;
83
142
  return counts;
84
143
  };
85
144
  var visibleTabs = (tabs, counts) => tabs.tabs.filter((t) => !t.offBoard || (counts[t.value] ?? 0) > 0);
86
- var blocksFor = (rows) => {
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
+ ];
87
158
  const hasStructure = rows.some((r) => r.parent || r.container);
88
- if (!hasStructure)
89
- return rows.map((row) => ({ kind: "issue", row, depth: 0 }));
159
+ if (!hasStructure) return rows.flatMap((row) => withLeaves(row, 0));
90
160
  const heads = new Map(rows.filter((r) => r.container).map((r) => [r.key, r]));
91
161
  const children = /* @__PURE__ */ new Map();
92
162
  const orphans = [];
@@ -102,36 +172,80 @@ var blocksFor = (rows) => {
102
172
  const groups = [];
103
173
  for (const [key, head] of heads)
104
174
  groups.push([
105
- { kind: "issue", row: head, depth: 0 },
106
- ...(children.get(key) ?? []).map((row) => ({
107
- kind: "issue",
108
- row,
109
- depth: 1
110
- }))
175
+ ...withLeaves(head, 0),
176
+ ...(children.get(key) ?? []).flatMap((row) => withLeaves(row, 1))
111
177
  ]);
112
178
  for (const [key, kids] of children) {
113
179
  if (heads.has(key)) continue;
180
+ const parent = known.get(key);
114
181
  groups.push([
115
- { kind: "fence", key, summary: kids[0].parent?.summary ?? "" },
116
- ...kids.map((row) => ({ kind: "issue", row, depth: 1 }))
182
+ {
183
+ kind: "fence",
184
+ key,
185
+ summary: kids[0].parent?.summary ?? "",
186
+ ...parent ? { parent } : {}
187
+ },
188
+ ...kids.flatMap((row) => withLeaves(row, 1))
117
189
  ]);
118
190
  }
119
191
  if (orphans.length)
120
192
  groups.push([
121
193
  ...groups.length ? [{ kind: "fence", key: null, summary: "No epic" }] : [],
122
- ...orphans.map((row) => ({ kind: "issue", row, depth: 0 }))
194
+ ...orphans.flatMap((row) => withLeaves(row, 0))
123
195
  ]);
124
196
  return groups.flatMap(
125
197
  (group, i) => i === 0 ? group : [{ kind: "gap" }, ...group]
126
198
  );
127
199
  };
128
- var blockIndexOfIssue = (blocks, issueIndex) => {
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) => {
129
213
  let seen = -1;
130
214
  for (let i = 0; i < blocks.length; i++) {
131
- if (blocks[i].kind === "issue" && ++seen === issueIndex) return i;
215
+ if (isStop(blocks[i]) && ++seen === stopIndex) return i;
132
216
  }
133
217
  return 0;
134
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
+ };
135
249
  var priorityGlyph = (name) => {
136
250
  const n = (name ?? "").trim().toLowerCase();
137
251
  if (/^(highest|blocker|critical|p[01])$/.test(n)) return "\u21C8";
@@ -166,6 +280,46 @@ var pillVariantFor = (row) => {
166
280
  if (t === "story") return "info";
167
281
  return "muted";
168
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
+ };
169
323
  var boardOf = async (client, jql, { tabs, board, limit = 200 } = {}) => {
170
324
  const [issues, derived] = await Promise.all([
171
325
  client.searchIssues(jql, { limit }),
@@ -358,7 +512,11 @@ var issueDetailOf = async (client, baseUrl, key) => {
358
512
  var transitionsOf = async (client, key) => (await client.getTransitions(key)).transitions.map((t) => ({
359
513
  id: t.id,
360
514
  name: t.name,
361
- 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
+ }
362
520
  }));
363
521
  var useTermSize = () => {
364
522
  const { stdout } = useStdout();
@@ -435,12 +593,13 @@ var IssueBody = ({
435
593
  );
436
594
  };
437
595
  var CHROME = 6;
596
+ var ROW_CHROME = 7;
438
597
  var LEGEND = [
439
598
  ["\u21C8 \u2191", "priority above the default"],
440
599
  ["=", "default priority"],
441
600
  ["\u2193 \u21CA", "priority below the default"],
442
601
  ["\u2500\u2500 epic \u2500\u2500", "a parent not in this tab; its rows hang beneath"],
443
- ["\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"],
444
603
  ["story \xB7 bug \xB7 task", "issue type"],
445
604
  ["2d", "time since last update"]
446
605
  ];
@@ -460,8 +619,21 @@ var tabLegendLine = (model) => model.tabs.source === "categories" ? [
460
619
  "To do \xB7 In progress \xB7 Done",
461
620
  "status category \u2014 not any one board's column"
462
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
+ };
463
633
  var IssueBoard = ({
464
634
  model,
635
+ leaves,
636
+ parents,
465
637
  viewer,
466
638
  loadedAt,
467
639
  scope,
@@ -499,7 +671,11 @@ var IssueBoard = ({
499
671
  (r) => `${r.key} ${r.summary}`.toLowerCase().includes(words)
500
672
  );
501
673
  }, [model.rows, search]);
502
- 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);
503
679
  const shown = visibleTabs(model.tabs, counts);
504
680
  const tabItems = shown.map((t) => ({
505
681
  value: t.value,
@@ -513,18 +689,26 @@ var IssueBoard = ({
513
689
  isActive: listFocused
514
690
  });
515
691
  const tab = active ?? initial ?? "";
692
+ const under = leaves?.under;
516
693
  const blocks = useMemo(
517
- () => blocksFor(narrowed.filter((r) => tabOf(r, model.tabs) === tab)),
518
- [narrowed, model.tabs, tab]
519
- );
520
- const issues = blocks.filter(
521
- (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]
522
700
  );
523
- const { cursor, setCursor } = useListCursor(issues.length, {
701
+ const stops = stopsOf(blocks);
702
+ const { cursor, setCursor } = useListCursor(stops.length, {
524
703
  vimKeys: true,
525
704
  isActive: listFocused
526
705
  });
527
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
+ };
528
712
  useInput((input, key) => {
529
713
  if (search.open) {
530
714
  if (key.escape) setSearch((s) => ({ ...s, open: false }));
@@ -539,7 +723,7 @@ var IssueBoard = ({
539
723
  if (input === "?" || key.escape) setLegend(false);
540
724
  return;
541
725
  }
542
- if (key.return && issues[cursor]) onOpen(issues[cursor].row.key);
726
+ if (key.return) openStop(stops[cursor]);
543
727
  if (input === "/")
544
728
  setSearch((s) => ({
545
729
  ...s,
@@ -554,7 +738,7 @@ var IssueBoard = ({
554
738
  const searching = search.open || scope.kind === "search";
555
739
  const chrome = CHROME + (searching ? 1 : 0) + (searchError ? 1 : 0);
556
740
  const size = Math.max(3, height - chrome);
557
- const focus = blockIndexOfIssue(blocks, cursor);
741
+ const focus = blockIndexOfStop(blocks, cursor);
558
742
  const { start, end } = windowFor(focus, blocks.length, size);
559
743
  const keyWidth = Math.max(8, ...narrowed.map((r) => r.key.length));
560
744
  const typeWidth = Math.max(
@@ -625,20 +809,61 @@ var IssueBoard = ({
625
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: [
626
810
  /* @__PURE__ */ jsx(Box, { width: 28, flexShrink: 0, children: /* @__PURE__ */ jsx(Text, { color: colors.accent, children: glyph }) }),
627
811
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: meaning })
628
- ] }, glyph)) : issues.length === 0 ? /* @__PURE__ */ jsx(Box, { paddingLeft: 4, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: emptyHint() }) }) : blocks.slice(start, end).map(
629
- (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(
630
- block.key ? `${block.summary} \xB7 ${block.key}` : block.summary,
631
- width - 7
632
- ) }) }, `f:${block.key ?? "none"}`) : /* @__PURE__ */ jsxs(SelectableRow, { active: start + i === focus, children: [
633
- 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,
634
852
  /* @__PURE__ */ jsx(Box, { flexShrink: 0, width: 2, children: /* @__PURE__ */ jsx(Text, { color: priorityMarker(block.row.priority).color, children: priorityMarker(block.row.priority).marker }) }),
635
- /* @__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 }) }),
636
854
  /* @__PURE__ */ jsx(Box, { flexShrink: 0, width: typeWidth + 2, children: /* @__PURE__ */ jsx(Pill, { tone: "soft", variant: pillVariantFor(block.row), children: block.row.type.toLowerCase() }) }),
637
- /* @__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
+ ),
638
863
  /* @__PURE__ */ jsx(Box, { flexShrink: 0, width: 5, justifyContent: "flex-end", children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: relativeAge(block.row.updated, now) }) })
639
- ] }, block.row.key)
640
- ) }),
641
- /* @__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}` : " " }) })
642
867
  ] });
643
868
  if (frame) return frame({ facts, title, hints, body });
644
869
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
@@ -747,4 +972,4 @@ var mockBoard = () => ({
747
972
  tabs: tabsFromCategories()
748
973
  });
749
974
 
750
- 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.8.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",