@markii/ansi 0.15.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ansi.d.ts +26 -0
- package/dist/ansi.js +51 -0
- package/dist/components/badge.d.ts +1 -1
- package/dist/components/badge.js +5 -4
- package/dist/components/callout.d.ts +24 -13
- package/dist/components/callout.js +45 -21
- package/dist/components/card.d.ts +24 -2
- package/dist/components/card.js +41 -12
- package/dist/components/cell.d.ts +5 -11
- package/dist/components/cell.js +11 -15
- package/dist/components/chart.d.ts +4 -10
- package/dist/components/chart.js +9 -16
- package/dist/components/details-string.d.ts +26 -0
- package/dist/components/details-string.js +39 -0
- package/dist/components/details.d.ts +30 -10
- package/dist/components/details.js +27 -25
- package/dist/components/divider.js +9 -11
- package/dist/components/figure.d.ts +5 -16
- package/dist/components/figure.js +9 -19
- package/dist/components/index.d.ts +14 -10
- package/dist/components/index.js +19 -16
- package/dist/components/kbd.d.ts +1 -1
- package/dist/components/kbd.js +3 -2
- package/dist/components/layout-wrapper.d.ts +7 -22
- package/dist/components/layout-wrapper.js +9 -23
- package/dist/components/progress.d.ts +3 -11
- package/dist/components/progress.js +6 -14
- package/dist/components/rating.d.ts +1 -2
- package/dist/components/rating.js +2 -3
- package/dist/components/row.d.ts +33 -20
- package/dist/components/row.js +33 -51
- package/dist/components/stat.d.ts +3 -8
- package/dist/components/stat.js +4 -9
- package/dist/components/tab.d.ts +9 -8
- package/dist/components/tab.js +12 -10
- package/dist/components/table-grid.js +1 -2
- package/dist/components/table.d.ts +5 -10
- package/dist/components/table.js +7 -12
- package/dist/components/tabs-string.d.ts +17 -0
- package/dist/components/tabs-string.js +30 -0
- package/dist/components/tabs.d.ts +29 -14
- package/dist/components/tabs.js +37 -30
- package/dist/index.d.ts +2 -4
- package/dist/index.js +6 -7
- package/dist/ink-string.d.ts +12 -0
- package/dist/ink-string.js +41 -0
- package/dist/interactive.d.ts +41 -0
- package/dist/interactive.js +64 -0
- package/dist/layout.js +1 -2
- package/dist/registry.d.ts +91 -100
- package/dist/registry.js +17 -9
- package/dist/render.d.ts +39 -24
- package/dist/render.js +676 -373
- package/dist/text-grid.d.ts +54 -0
- package/dist/text-grid.js +249 -0
- package/package.json +7 -4
- package/dist/box.d.ts +0 -47
- package/dist/box.js +0 -209
- package/dist/measure.d.ts +0 -25
- package/dist/measure.js +0 -108
package/dist/components/row.d.ts
CHANGED
|
@@ -1,26 +1,39 @@
|
|
|
1
|
-
import type { AnsiComponent } from '../registry.js';
|
|
1
|
+
import type { AnsiComponent, DirectiveAttributes } from '../registry.js';
|
|
2
|
+
declare const TEXT_ALIGNS: readonly ["left", "center", "right"];
|
|
3
|
+
export type RowTextAlign = (typeof TEXT_ALIGNS)[number];
|
|
2
4
|
/** Below this width, cells stack vertically instead of sitting side by side — a named constant per the batch brief, not a magic number. */
|
|
3
5
|
export declare const ROW_COLUMN_THRESHOLD = 60;
|
|
6
|
+
export interface RowLayout {
|
|
7
|
+
align: RowTextAlign;
|
|
8
|
+
columnCount: number;
|
|
9
|
+
colWidth: number;
|
|
10
|
+
stacked: boolean;
|
|
11
|
+
}
|
|
4
12
|
/**
|
|
5
|
-
* `:::row{cols=2|3|4 text=left|center|right} ... :::`
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
13
|
+
* `:::row{cols=2|3|4 text=left|center|right} ... :::`'s pure layout
|
|
14
|
+
* decision: how many columns, how wide each one is, and whether the row
|
|
15
|
+
* stacks. `render.tsx`'s walk calls this directly (rather than `row`'s own
|
|
16
|
+
* registered component) so it can build each cell's real Ink subtree at the
|
|
17
|
+
* correct width BEFORE assembling the row's flex `Box` — see `render.tsx`'s
|
|
18
|
+
* top comment and `registry.ts`'s doc comment on why this one directive is
|
|
19
|
+
* handled at the walk level. Kept here, not in `render.tsx`, so
|
|
20
|
+
* `components/contract-drift.test.ts`'s literal source scan of `row.tsx`
|
|
21
|
+
* still finds the `attributes.cols`/`attributes.text` reads.
|
|
14
22
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
23
|
+
* An absent/invalid `cols` auto-fits: every cell becomes one column, all in
|
|
24
|
+
* a single grid row. A `cols` value smaller than the cell count wraps into
|
|
25
|
+
* further grid rows, `cols` cells at a time. Below `ROW_COLUMN_THRESHOLD`
|
|
26
|
+
* columns, or with only one cell, cells stack vertically instead.
|
|
27
|
+
*/
|
|
28
|
+
export declare function resolveRowLayout(attributes: DirectiveAttributes, cellCount: number, width: number): RowLayout;
|
|
29
|
+
/**
|
|
30
|
+
* `row`'s registered component: a passthrough. `render.tsx`'s walk builds
|
|
31
|
+
* the row's REAL flex-`Box` grid (or, for a `row` reached through STRING
|
|
32
|
+
* mode nested inside a self-drawing container, a flattened string) directly
|
|
33
|
+
* and hands it down as `children`, so the registered component's own job is
|
|
34
|
+
* just to return what it was given. Kept as a real registration (rather
|
|
35
|
+
* than omitted) so alias resolution, the contract-drift coverage test, and
|
|
36
|
+
* an unexpected direct invocation all still see a real `row` component.
|
|
25
37
|
*/
|
|
26
38
|
export declare const Row: AnsiComponent;
|
|
39
|
+
export {};
|
package/dist/components/row.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { columns } from '../box.js';
|
|
2
1
|
const COLS_VALUES = ['2', '3', '4'];
|
|
3
2
|
function isColsValue(value) {
|
|
4
3
|
return COLS_VALUES.includes(value);
|
|
@@ -9,59 +8,42 @@ function isTextAlign(value) {
|
|
|
9
8
|
}
|
|
10
9
|
/** Below this width, cells stack vertically instead of sitting side by side — a named constant per the batch brief, not a magic number. */
|
|
11
10
|
export const ROW_COLUMN_THRESHOLD = 60;
|
|
12
|
-
/** Spaces between adjacent columns. */
|
|
13
|
-
const GUTTER = 1;
|
|
14
11
|
/**
|
|
15
|
-
* `:::row{cols=2|3|4 text=left|center|right} ... :::`
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
12
|
+
* `:::row{cols=2|3|4 text=left|center|right} ... :::`'s pure layout
|
|
13
|
+
* decision: how many columns, how wide each one is, and whether the row
|
|
14
|
+
* stacks. `render.tsx`'s walk calls this directly (rather than `row`'s own
|
|
15
|
+
* registered component) so it can build each cell's real Ink subtree at the
|
|
16
|
+
* correct width BEFORE assembling the row's flex `Box` — see `render.tsx`'s
|
|
17
|
+
* top comment and `registry.ts`'s doc comment on why this one directive is
|
|
18
|
+
* handled at the walk level. Kept here, not in `render.tsx`, so
|
|
19
|
+
* `components/contract-drift.test.ts`'s literal source scan of `row.tsx`
|
|
20
|
+
* still finds the `attributes.cols`/`attributes.text` reads.
|
|
24
21
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* that part's own `render({ width })` at the ACTUAL column width decided
|
|
30
|
-
* below, rather than rendered once at the row's full width and then
|
|
31
|
-
* re-wrapped into a narrower column. A cell containing a self-drawing box
|
|
32
|
-
* (a nested `card`/`table`) therefore draws that box at the real column
|
|
33
|
-
* width in the first place, instead of having an already-drawn frame
|
|
34
|
-
* mangled by a later re-wrap.
|
|
22
|
+
* An absent/invalid `cols` auto-fits: every cell becomes one column, all in
|
|
23
|
+
* a single grid row. A `cols` value smaller than the cell count wraps into
|
|
24
|
+
* further grid rows, `cols` cells at a time. Below `ROW_COLUMN_THRESHOLD`
|
|
25
|
+
* columns, or with only one cell, cells stack vertically instead.
|
|
35
26
|
*/
|
|
36
|
-
export
|
|
27
|
+
export function resolveRowLayout(attributes, cellCount, width) {
|
|
37
28
|
const rawTextAlign = attributes.text;
|
|
38
29
|
const align = rawTextAlign && isTextAlign(rawTextAlign) ? rawTextAlign : 'left';
|
|
39
|
-
const cellParts = children.parts;
|
|
40
|
-
if (cellParts.length === 0)
|
|
41
|
-
return '';
|
|
42
30
|
const rawCols = attributes.cols ?? '';
|
|
43
|
-
const requestedCols = isColsValue(rawCols)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const
|
|
47
|
-
const
|
|
48
|
-
?
|
|
49
|
-
:
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const rowCells = placedCells.slice(index, index + columnCount);
|
|
63
|
-
const widths = rowCells.map(() => colWidth);
|
|
64
|
-
gridRows.push(columns(rowCells, widths, GUTTER));
|
|
65
|
-
}
|
|
66
|
-
return gridRows.join('\n\n');
|
|
67
|
-
};
|
|
31
|
+
const requestedCols = isColsValue(rawCols) ? Number(rawCols) : cellCount;
|
|
32
|
+
const columnCount = Math.max(1, Math.min(requestedCols, Math.max(1, cellCount)));
|
|
33
|
+
const gutter = 1;
|
|
34
|
+
const stacked = width < ROW_COLUMN_THRESHOLD || columnCount <= 1;
|
|
35
|
+
const colWidth = stacked
|
|
36
|
+
? width
|
|
37
|
+
: Math.max(1, Math.floor((width - gutter * (columnCount - 1)) / columnCount));
|
|
38
|
+
return { align, columnCount, colWidth, stacked };
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* `row`'s registered component: a passthrough. `render.tsx`'s walk builds
|
|
42
|
+
* the row's REAL flex-`Box` grid (or, for a `row` reached through STRING
|
|
43
|
+
* mode nested inside a self-drawing container, a flattened string) directly
|
|
44
|
+
* and hands it down as `children`, so the registered component's own job is
|
|
45
|
+
* just to return what it was given. Kept as a real registration (rather
|
|
46
|
+
* than omitted) so alias resolution, the contract-drift coverage test, and
|
|
47
|
+
* an unexpected direct invocation all still see a real `row` component.
|
|
48
|
+
*/
|
|
49
|
+
export const Row = ({ children }) => children;
|
|
@@ -1,13 +1,8 @@
|
|
|
1
1
|
import type { AnsiComponent } from '../registry.js';
|
|
2
2
|
/**
|
|
3
3
|
* `::stat{value=42 label="stars" trend=up}` — a big value plus label. Data
|
|
4
|
-
* binding (§8)
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* a blank line. Terminal form: a muted label line, then a bold value line
|
|
8
|
-
* (with `delta` appended, colored by `trend` when recognized). A failed or
|
|
9
|
-
* stale binding appends `failure-presentation.ts`'s quiet suffix to the
|
|
10
|
-
* value line — the terminal has no tooltip channel, so the reason has to
|
|
11
|
-
* reach the text itself (AGENTS.md's "clean is not silent").
|
|
4
|
+
* binding (§8): a bound number/string supplies `value`; a bound object may
|
|
5
|
+
* supply `value`/`label`/`delta`/`trend` — an explicit attribute always
|
|
6
|
+
* wins. Missing value renders `—` rather than a blank line.
|
|
12
7
|
*/
|
|
13
8
|
export declare const Stat: AnsiComponent;
|
package/dist/components/stat.js
CHANGED
|
@@ -35,16 +35,11 @@ function pick(attribute, fromData) {
|
|
|
35
35
|
}
|
|
36
36
|
/**
|
|
37
37
|
* `::stat{value=42 label="stars" trend=up}` — a big value plus label. Data
|
|
38
|
-
* binding (§8)
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* a blank line. Terminal form: a muted label line, then a bold value line
|
|
42
|
-
* (with `delta` appended, colored by `trend` when recognized). A failed or
|
|
43
|
-
* stale binding appends `failure-presentation.ts`'s quiet suffix to the
|
|
44
|
-
* value line — the terminal has no tooltip channel, so the reason has to
|
|
45
|
-
* reach the text itself (AGENTS.md's "clean is not silent").
|
|
38
|
+
* binding (§8): a bound number/string supplies `value`; a bound object may
|
|
39
|
+
* supply `value`/`label`/`delta`/`trend` — an explicit attribute always
|
|
40
|
+
* wins. Missing value renders `—` rather than a blank line.
|
|
46
41
|
*/
|
|
47
|
-
export const Stat = (attributes,
|
|
42
|
+
export const Stat = ({ attributes, ctx }) => {
|
|
48
43
|
const { data, dataStatus, dataFailureKind } = ctx;
|
|
49
44
|
const bound = safeRead(() => dataStatus === 'missing' || dataStatus === 'error'
|
|
50
45
|
? {}
|
package/dist/components/tab.d.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type AnsiComponent } from '../registry.js';
|
|
2
2
|
export declare const DEFAULT_TAB_LABEL = "Tab";
|
|
3
3
|
/**
|
|
4
4
|
* `:::tab{label="..."} ... :::` — one panel of a `tabs` component
|
|
5
|
-
* (`tabs.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
5
|
+
* (`tabs.tsx`). Rendered STANDALONE (outside a `tabs` parent, or when
|
|
6
|
+
* reached through `render.tsx`'s STRING-mode fallback), it reads its own
|
|
7
|
+
* `label` attribute directly and shows a bold heading above the panel body.
|
|
8
|
+
* When nested under a real `tabs` directive on the ELEMENT-mode path,
|
|
9
|
+
* `render.tsx`'s `extractTabPanelsElement` reads `label` itself and this
|
|
10
|
+
* component is never invoked for that child — `InteractiveTabs` draws the
|
|
11
|
+
* heading instead, since it needs to draw ALL headings together (the tab
|
|
12
|
+
* strip / stacked list), not one at a time.
|
|
12
13
|
*/
|
|
13
14
|
export declare const Tab: AnsiComponent;
|
package/dist/components/tab.js
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
|
+
import { childrenText } from '../registry.js';
|
|
1
2
|
export const DEFAULT_TAB_LABEL = 'Tab';
|
|
2
3
|
/**
|
|
3
4
|
* `:::tab{label="..."} ... :::` — one panel of a `tabs` component
|
|
4
|
-
* (`tabs.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
5
|
+
* (`tabs.tsx`). Rendered STANDALONE (outside a `tabs` parent, or when
|
|
6
|
+
* reached through `render.tsx`'s STRING-mode fallback), it reads its own
|
|
7
|
+
* `label` attribute directly and shows a bold heading above the panel body.
|
|
8
|
+
* When nested under a real `tabs` directive on the ELEMENT-mode path,
|
|
9
|
+
* `render.tsx`'s `extractTabPanelsElement` reads `label` itself and this
|
|
10
|
+
* component is never invoked for that child — `InteractiveTabs` draws the
|
|
11
|
+
* heading instead, since it needs to draw ALL headings together (the tab
|
|
12
|
+
* strip / stacked list), not one at a time.
|
|
11
13
|
*/
|
|
12
|
-
export const Tab = (attributes, children, ctx) => {
|
|
14
|
+
export const Tab = ({ attributes, children, ctx }) => {
|
|
13
15
|
const label = attributes.label ?? DEFAULT_TAB_LABEL;
|
|
14
16
|
const heading = ctx.bold(ctx.text(label));
|
|
15
|
-
const
|
|
16
|
-
return
|
|
17
|
+
const body = childrenText(children);
|
|
18
|
+
return body ? `${heading}\n${body}` : heading;
|
|
17
19
|
};
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { pad, wrap } from '../
|
|
2
|
-
import { measure } from '../measure.js';
|
|
1
|
+
import { padText as pad, wrapText as wrap, measureWidth as measure, } from '../text-grid.js';
|
|
3
2
|
/**
|
|
4
3
|
* The ONE box-drawing/width-negotiation routine every table this engine
|
|
5
4
|
* draws goes through: the data-bound `::table` component (`./table.ts`) and
|
|
@@ -1,16 +1,11 @@
|
|
|
1
1
|
import type { AnsiComponent } from '../registry.js';
|
|
2
2
|
/**
|
|
3
3
|
* `::table{data=users columns="name,role" limit=10}` — a data-bound table.
|
|
4
|
-
* Data binding (§8)
|
|
5
|
-
*
|
|
6
|
-
* `format`/`decimals` apply to numeric cells only; `text` aligns every
|
|
7
|
-
* cell's content within its column; `caption`, when given, is a bold line
|
|
8
|
-
* above the grid. A missing/stale/failed binding degrades to the same quiet
|
|
9
|
-
* "no data" line `chart`/`progress` use, with the failure suffix appended.
|
|
4
|
+
* Data binding (§8): `@markii/stdlib`'s `deriveTableShape` decides the
|
|
5
|
+
* layout from the bound value's own shape.
|
|
10
6
|
*
|
|
11
|
-
* Registered `selfLayout
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* the borders.
|
|
7
|
+
* Registered `selfLayout`: draws real box-drawing glyphs
|
|
8
|
+
* (`./table-grid.ts`), so it sizes its own grid off `ctx.layout` instead of
|
|
9
|
+
* letting a generic post-render narrow/pad corrupt the borders.
|
|
15
10
|
*/
|
|
16
11
|
export declare const Table: AnsiComponent;
|
package/dist/components/table.js
CHANGED
|
@@ -24,7 +24,7 @@ function parseLimit(raw) {
|
|
|
24
24
|
function limitRows(rows, limit) {
|
|
25
25
|
return limit === undefined ? rows : rows.slice(0, limit);
|
|
26
26
|
}
|
|
27
|
-
/** Renders one cell's display text: `format`/`decimals` apply only when the cell's raw value is itself numeric-like
|
|
27
|
+
/** Renders one cell's display text: `format`/`decimals` apply only when the cell's raw value is itself numeric-like. */
|
|
28
28
|
function renderCell(value, format, decimals) {
|
|
29
29
|
if (format && isNumericLike(value))
|
|
30
30
|
return formatValue(value, format, decimals);
|
|
@@ -36,19 +36,14 @@ function isTextAlign(value) {
|
|
|
36
36
|
}
|
|
37
37
|
/**
|
|
38
38
|
* `::table{data=users columns="name,role" limit=10}` — a data-bound table.
|
|
39
|
-
* Data binding (§8)
|
|
40
|
-
*
|
|
41
|
-
* `format`/`decimals` apply to numeric cells only; `text` aligns every
|
|
42
|
-
* cell's content within its column; `caption`, when given, is a bold line
|
|
43
|
-
* above the grid. A missing/stale/failed binding degrades to the same quiet
|
|
44
|
-
* "no data" line `chart`/`progress` use, with the failure suffix appended.
|
|
39
|
+
* Data binding (§8): `@markii/stdlib`'s `deriveTableShape` decides the
|
|
40
|
+
* layout from the bound value's own shape.
|
|
45
41
|
*
|
|
46
|
-
* Registered `selfLayout
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
* the borders.
|
|
42
|
+
* Registered `selfLayout`: draws real box-drawing glyphs
|
|
43
|
+
* (`./table-grid.ts`), so it sizes its own grid off `ctx.layout` instead of
|
|
44
|
+
* letting a generic post-render narrow/pad corrupt the borders.
|
|
50
45
|
*/
|
|
51
|
-
export const Table = (attributes,
|
|
46
|
+
export const Table = ({ attributes, ctx }) => {
|
|
52
47
|
const { data, dataStatus, dataFailureKind } = ctx;
|
|
53
48
|
const columnsOverride = parseColumns(attributes.columns);
|
|
54
49
|
const limit = parseLimit(attributes.limit);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type AnsiComponent } from '../registry.js';
|
|
2
|
+
/**
|
|
3
|
+
* The STRING-mode `tabs` registration — the fallback rendering used only
|
|
4
|
+
* when a `tabs` directive is reached through `render.tsx`'s STRING mode
|
|
5
|
+
* (nested inside a self-drawing container) AND, unlike `row`, not
|
|
6
|
+
* reachable through the panel-extraction path there either (both of
|
|
7
|
+
* `render.tsx`'s STRING and ELEMENT dispatchers for `tabs` build panels
|
|
8
|
+
* directly from the hast tree via `extractTabPanels`/`extractTabPanelsElement`
|
|
9
|
+
* rather than calling this component, since only the hast tree — not an
|
|
10
|
+
* already-flattened string — can tell each `tab` child's own `label` apart
|
|
11
|
+
* from its neighbor without a fragile heuristic). This registration exists
|
|
12
|
+
* so the registry always has a real `tabs` component (alias resolution,
|
|
13
|
+
* the contract-drift coverage test, and a caller invoking it directly all
|
|
14
|
+
* see one), using the same blank-line-boundary heuristic the pre-Ink engine
|
|
15
|
+
* used when it has nothing but a flattened string to work with.
|
|
16
|
+
*/
|
|
17
|
+
export declare const Tabs: AnsiComponent;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { childrenText } from '../registry.js';
|
|
2
|
+
/** The visible marker appended to whichever tab is treated as "active". */
|
|
3
|
+
const ACTIVE_MARKER = ' (active)';
|
|
4
|
+
/**
|
|
5
|
+
* The STRING-mode `tabs` registration — the fallback rendering used only
|
|
6
|
+
* when a `tabs` directive is reached through `render.tsx`'s STRING mode
|
|
7
|
+
* (nested inside a self-drawing container) AND, unlike `row`, not
|
|
8
|
+
* reachable through the panel-extraction path there either (both of
|
|
9
|
+
* `render.tsx`'s STRING and ELEMENT dispatchers for `tabs` build panels
|
|
10
|
+
* directly from the hast tree via `extractTabPanels`/`extractTabPanelsElement`
|
|
11
|
+
* rather than calling this component, since only the hast tree — not an
|
|
12
|
+
* already-flattened string — can tell each `tab` child's own `label` apart
|
|
13
|
+
* from its neighbor without a fragile heuristic). This registration exists
|
|
14
|
+
* so the registry always has a real `tabs` component (alias resolution,
|
|
15
|
+
* the contract-drift coverage test, and a caller invoking it directly all
|
|
16
|
+
* see one), using the same blank-line-boundary heuristic the pre-Ink engine
|
|
17
|
+
* used when it has nothing but a flattened string to work with.
|
|
18
|
+
*/
|
|
19
|
+
export const Tabs = ({ children }) => {
|
|
20
|
+
const text = childrenText(children);
|
|
21
|
+
if (!text.trim())
|
|
22
|
+
return '';
|
|
23
|
+
const blocks = text.split('\n\n');
|
|
24
|
+
const [first, ...rest] = blocks;
|
|
25
|
+
if (first === undefined)
|
|
26
|
+
return text;
|
|
27
|
+
const lines = first.split('\n');
|
|
28
|
+
lines[0] = `${lines[0] ?? ''}${ACTIVE_MARKER}`;
|
|
29
|
+
return [lines.join('\n'), ...rest].join('\n\n');
|
|
30
|
+
};
|
|
@@ -1,18 +1,33 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { ColorLevel } from '../ansi.js';
|
|
3
|
+
import type { ElementTabPanel } from '../render.js';
|
|
2
4
|
/**
|
|
3
5
|
* `::::tabs :::tab{label="..."} ... ::: :::tab{label="..."} ... ::: ::::` —
|
|
4
|
-
* a tabbed panel switcher
|
|
5
|
-
* `
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* a tabbed panel switcher, built as a real Ink element (`render.tsx`'s
|
|
7
|
+
* `renderTabsElement` calls this directly rather than going through the
|
|
8
|
+
* ordinary registry-component invocation, so it can hand it real per-panel
|
|
9
|
+
* subtrees — see `registry.ts`'s and `render.tsx`'s doc comments).
|
|
8
10
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* the
|
|
11
|
+
* Non-interactive (the default, and the render-once path): every panel is
|
|
12
|
+
* shown, stacked, in document order, the first one's heading marked
|
|
13
|
+
* `(active)` — matching the pre-Ink engine's faithfulness limitation of
|
|
14
|
+
* showing every panel rather than picking one, since a static render has no
|
|
15
|
+
* way to ask which panel a reader wants.
|
|
16
|
+
*
|
|
17
|
+
* Interactive: only the active panel's body is shown; the header row lists
|
|
18
|
+
* every label, the active one bracketed; left/right or Tab switches it when
|
|
19
|
+
* this `tabs` instance is focused (`useIsFocused`). When this `tabs`
|
|
20
|
+
* instance itself is the FOCUSED component (as opposed to merely being
|
|
21
|
+
* interactive), the whole header strip is drawn in the accent color and
|
|
22
|
+
* prefixed with a `› ` glyph (`useFocusMarker`), so focus is visible even
|
|
23
|
+
* on a theme with no perceptible accent color; an unfocused header strip
|
|
24
|
+
* gets two leading spaces instead, so nothing shifts horizontally when
|
|
25
|
+
* focus moves onto or off of this block.
|
|
17
26
|
*/
|
|
18
|
-
export
|
|
27
|
+
export interface InteractiveTabsProps {
|
|
28
|
+
panels: readonly ElementTabPanel[];
|
|
29
|
+
color: ColorLevel;
|
|
30
|
+
interactive: boolean;
|
|
31
|
+
focusId: number | undefined;
|
|
32
|
+
}
|
|
33
|
+
export declare function InteractiveTabs({ panels, color, interactive, focusId, }: InteractiveTabsProps): ReactNode;
|
package/dist/components/tabs.js
CHANGED
|
@@ -1,30 +1,37 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { Box, Text, useInput } from 'ink';
|
|
4
|
+
import { bold, dim } from '../ansi.js';
|
|
5
|
+
import { useFocusMarker, useIsFocused } from '../interactive.js';
|
|
6
|
+
export function InteractiveTabs({ panels, color, interactive, focusId, }) {
|
|
7
|
+
const [active, setActive] = useState(0);
|
|
8
|
+
const focused = useIsFocused(focusId);
|
|
9
|
+
useInput((_input, key) => {
|
|
10
|
+
if (key.leftArrow) {
|
|
11
|
+
setActive((current) => (current - 1 + panels.length) % panels.length);
|
|
12
|
+
}
|
|
13
|
+
else if (key.rightArrow || key.tab) {
|
|
14
|
+
setActive((current) => (current + 1) % panels.length);
|
|
15
|
+
}
|
|
16
|
+
}, { isActive: interactive && focused });
|
|
17
|
+
// Every hook this component calls runs BEFORE the non-interactive early
|
|
18
|
+
// return below, so the hook order is identical in both modes. The header
|
|
19
|
+
// strip is cheap to build and unused in the non-interactive branch; that
|
|
20
|
+
// waste is the price of never making a hook call conditional.
|
|
21
|
+
const activeIndex = Math.min(active, Math.max(0, panels.length - 1));
|
|
22
|
+
const activePanel = panels[activeIndex];
|
|
23
|
+
const strip = panels
|
|
24
|
+
.map((panel, index) => {
|
|
25
|
+
const rendered = index === activeIndex
|
|
26
|
+
? bold(`[${panel.label}]`, color)
|
|
27
|
+
: dim(panel.label, color);
|
|
28
|
+
return index > 0 ? ` ${rendered}` : rendered;
|
|
29
|
+
})
|
|
30
|
+
.join('');
|
|
31
|
+
const focusGlyph = focused ? '› ' : ' ';
|
|
32
|
+
const headingLine = useFocusMarker(`${focusGlyph}${strip}`, focusId);
|
|
33
|
+
if (!interactive) {
|
|
34
|
+
return (_jsx(Box, { flexDirection: "column", children: panels.map((panel, index) => (_jsxs(Box, { marginTop: index > 0 ? 1 : 0, flexDirection: "column", children: [_jsx(Text, { children: bold(`${panel.label}${index === 0 ? ' (active)' : ''}`, color) }), panel.body] }, index))) }));
|
|
35
|
+
}
|
|
36
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: headingLine }), activePanel?.body] }));
|
|
37
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
|
-
export { renderMarkToAnsi, renderMarkNodeToAnsi, renderMarkInlineToAnsi, type RenderMarkOptions, } from './render.js';
|
|
1
|
+
export { renderMarkToAnsi, renderMarkNodeToAnsi, renderMarkInlineToAnsi, buildMarkElement, type RenderMarkOptions, } from './render.js';
|
|
2
2
|
export { fg, colorize, bold, dim, italic, underline, inverse, hyperlink, detectColorLevel, resolveColorOption, type ColorLevel, type ColorOption, type AnsiColor, } from './ansi.js';
|
|
3
3
|
export { defaultAnsiTheme, type AnsiTheme, type Tier1Token } from './theme.js';
|
|
4
4
|
export { style } from './style.js';
|
|
5
|
-
export { createAnsiRegistry, mergeAnsiRegistries, registryAliases, readRegistryComponent, resolveDirectiveAlias, REGISTRY_ALIASES, type DirectiveAttributes, type AnsiRegistry, type AnsiRegistryEntry, type AnsiComponent, type
|
|
5
|
+
export { createAnsiRegistry, mergeAnsiRegistries, registryAliases, readRegistryComponent, resolveDirectiveAlias, childrenText, REGISTRY_ALIASES, type DirectiveAttributes, type AnsiRegistry, type AnsiRegistryEntry, type AnsiComponent, type AnsiComponentProps, type AnsiRenderContext, type RegistryAlias, type RegistryAliases, type ResolvedDirective, type ValueResolution, } from './registry.js';
|
|
6
6
|
export { resolveStorePath, resolveScopedPath, VAULT_NAME_PREFIX, type StorePathResolution, type ValueScope, } from './resolve.js';
|
|
7
7
|
export { failurePhrase, failureTitle, failureToken, dataStateSuffix, emptyInlineTitle, invalidAttributeValueLabel, invalidAttributeValueTitle, unsafeImageSrcLabel, unsafeImageSrcTitle, } from './failure-presentation.js';
|
|
8
8
|
export { stringifyStoredValue } from './value-format.js';
|
|
9
|
-
export { measure, stripAnsi } from './measure.js';
|
|
10
9
|
export { stripControlCharacters, sanitizeBlockText, sanitizeUrlText, BLOCK_TAB_WIDTH, } from './sanitize.js';
|
|
11
|
-
export { wrap, pad, indentBlock, columns, frame, rule, type FrameOptions, } from './box.js';
|
|
12
10
|
export { resolveLayoutAttributes, applyLayout, LAYOUT_ATTRIBUTE_KEYS, type ResolvedLayoutAttributes, type ResolvedLayoutPresets, type WidthPreset, type AlignPreset, } from './layout.js';
|
|
13
11
|
export { type ResolveImageSrc } from './image-resolve.js';
|
|
14
12
|
export { type ResolveHref } from './href-resolve.js';
|
package/dist/index.js
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
|
-
// @markii/ansi: a
|
|
1
|
+
// @markii/ansi: a terminal renderer for Markii documents, built on Ink. It
|
|
2
2
|
// consumes @markii/core's sanitized hast and emits a plain string (optionally
|
|
3
3
|
// carrying ANSI SGR/OSC 8 escapes) for a terminal, a pipe, or any host that
|
|
4
|
-
// wants text with no HTML
|
|
5
|
-
//
|
|
6
|
-
|
|
4
|
+
// wants text with no HTML runtime — plus, for a live viewer, the Ink element
|
|
5
|
+
// tree itself (`buildMarkElement`). It is a third platform renderer alongside
|
|
6
|
+
// @markii/html and @markii/react.
|
|
7
|
+
export { renderMarkToAnsi, renderMarkNodeToAnsi, renderMarkInlineToAnsi, buildMarkElement, } from './render.js';
|
|
7
8
|
export { fg, colorize, bold, dim, italic, underline, inverse, hyperlink, detectColorLevel, resolveColorOption, } from './ansi.js';
|
|
8
9
|
export { defaultAnsiTheme } from './theme.js';
|
|
9
10
|
export { style } from './style.js';
|
|
10
|
-
export { createAnsiRegistry, mergeAnsiRegistries, registryAliases, readRegistryComponent, resolveDirectiveAlias, REGISTRY_ALIASES, } from './registry.js';
|
|
11
|
+
export { createAnsiRegistry, mergeAnsiRegistries, registryAliases, readRegistryComponent, resolveDirectiveAlias, childrenText, REGISTRY_ALIASES, } from './registry.js';
|
|
11
12
|
export { resolveStorePath, resolveScopedPath, VAULT_NAME_PREFIX, } from './resolve.js';
|
|
12
13
|
export { failurePhrase, failureTitle, failureToken, dataStateSuffix, emptyInlineTitle, invalidAttributeValueLabel, invalidAttributeValueTitle, unsafeImageSrcLabel, unsafeImageSrcTitle, } from './failure-presentation.js';
|
|
13
14
|
export { stringifyStoredValue } from './value-format.js';
|
|
14
|
-
export { measure, stripAnsi } from './measure.js';
|
|
15
15
|
export { stripControlCharacters, sanitizeBlockText, sanitizeUrlText, BLOCK_TAB_WIDTH, } from './sanitize.js';
|
|
16
|
-
export { wrap, pad, indentBlock, columns, frame, rule, } from './box.js';
|
|
17
16
|
export { resolveLayoutAttributes, applyLayout, LAYOUT_ATTRIBUTE_KEYS, } from './layout.js';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ReactElement } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Mounts `element` with Ink at `columns` wide, captures the first (and only)
|
|
4
|
+
* frame as a plain string, and unmounts. `ink` is imported dynamically
|
|
5
|
+
* because `yoga-layout` (Ink's layout engine) resolves its WASM module via a
|
|
6
|
+
* top-level `await` at ES-module-evaluation time (`SPIKE-10-findings.md`'s
|
|
7
|
+
* "Item 3"): the first call to this function pays that one-time cost, and
|
|
8
|
+
* every exported entry point that reaches here is therefore `async`. A
|
|
9
|
+
* later call in the same process reuses the already-evaluated module graph,
|
|
10
|
+
* the ordinary ESM module-cache guarantee.
|
|
11
|
+
*/
|
|
12
|
+
export declare function renderInkToString(element: ReactElement, columns: number): Promise<string>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Writable } from 'node:stream';
|
|
2
|
+
function makeCollectingStream(columns) {
|
|
3
|
+
const chunks = [];
|
|
4
|
+
const stream = new Writable({
|
|
5
|
+
write(chunk, _encoding, callback) {
|
|
6
|
+
chunks.push(chunk.toString('utf8'));
|
|
7
|
+
callback();
|
|
8
|
+
},
|
|
9
|
+
});
|
|
10
|
+
stream.columns = columns;
|
|
11
|
+
stream.rows = 40;
|
|
12
|
+
// Ink's static/measured layout paths expect a TTY-shaped stream.
|
|
13
|
+
stream.isTTY = true;
|
|
14
|
+
stream.getColorDepth = () => 8;
|
|
15
|
+
stream.hasColors = () => true;
|
|
16
|
+
return { stream, getOutput: () => chunks.join('') };
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Mounts `element` with Ink at `columns` wide, captures the first (and only)
|
|
20
|
+
* frame as a plain string, and unmounts. `ink` is imported dynamically
|
|
21
|
+
* because `yoga-layout` (Ink's layout engine) resolves its WASM module via a
|
|
22
|
+
* top-level `await` at ES-module-evaluation time (`SPIKE-10-findings.md`'s
|
|
23
|
+
* "Item 3"): the first call to this function pays that one-time cost, and
|
|
24
|
+
* every exported entry point that reaches here is therefore `async`. A
|
|
25
|
+
* later call in the same process reuses the already-evaluated module graph,
|
|
26
|
+
* the ordinary ESM module-cache guarantee.
|
|
27
|
+
*/
|
|
28
|
+
export async function renderInkToString(element, columns) {
|
|
29
|
+
const { render } = await import('ink');
|
|
30
|
+
const { stream, getOutput } = makeCollectingStream(columns);
|
|
31
|
+
const instance = render(element, {
|
|
32
|
+
stdout: stream,
|
|
33
|
+
stdin: process.stdin,
|
|
34
|
+
exitOnCtrlC: false,
|
|
35
|
+
patchConsole: false,
|
|
36
|
+
debug: false,
|
|
37
|
+
interactive: false,
|
|
38
|
+
});
|
|
39
|
+
instance.unmount();
|
|
40
|
+
return getOutput();
|
|
41
|
+
}
|