@markii/ansi 0.15.0 → 0.17.0-next.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 (70) hide show
  1. package/LICENSE +21 -0
  2. package/dist/ansi.d.ts +26 -0
  3. package/dist/ansi.js +51 -0
  4. package/dist/components/badge.d.ts +1 -1
  5. package/dist/components/badge.js +5 -4
  6. package/dist/components/callout.d.ts +38 -13
  7. package/dist/components/callout.js +68 -31
  8. package/dist/components/card.d.ts +34 -2
  9. package/dist/components/card.js +55 -18
  10. package/dist/components/cell.d.ts +5 -11
  11. package/dist/components/cell.js +11 -15
  12. package/dist/components/chart.d.ts +4 -10
  13. package/dist/components/chart.js +16 -24
  14. package/dist/components/details-string.d.ts +26 -0
  15. package/dist/components/details-string.js +39 -0
  16. package/dist/components/details.d.ts +32 -10
  17. package/dist/components/details.js +28 -25
  18. package/dist/components/divider.js +12 -16
  19. package/dist/components/figure.d.ts +5 -16
  20. package/dist/components/figure.js +9 -19
  21. package/dist/components/index.d.ts +14 -10
  22. package/dist/components/index.js +19 -16
  23. package/dist/components/kbd.d.ts +1 -1
  24. package/dist/components/kbd.js +3 -2
  25. package/dist/components/layout-wrapper.d.ts +7 -22
  26. package/dist/components/layout-wrapper.js +9 -23
  27. package/dist/components/progress.d.ts +3 -11
  28. package/dist/components/progress.js +8 -17
  29. package/dist/components/rating.d.ts +1 -2
  30. package/dist/components/rating.js +3 -4
  31. package/dist/components/row.d.ts +35 -20
  32. package/dist/components/row.js +35 -51
  33. package/dist/components/stat.d.ts +3 -8
  34. package/dist/components/stat.js +5 -11
  35. package/dist/components/tab.d.ts +9 -8
  36. package/dist/components/tab.js +12 -10
  37. package/dist/components/table-grid.d.ts +2 -1
  38. package/dist/components/table-grid.js +14 -12
  39. package/dist/components/table.d.ts +5 -10
  40. package/dist/components/table.js +8 -13
  41. package/dist/components/tabs-string.d.ts +17 -0
  42. package/dist/components/tabs-string.js +30 -0
  43. package/dist/components/tabs.d.ts +31 -14
  44. package/dist/components/tabs.js +55 -30
  45. package/dist/failure-presentation.d.ts +15 -0
  46. package/dist/failure-presentation.js +22 -0
  47. package/dist/glyphs.d.ts +73 -0
  48. package/dist/glyphs.js +88 -0
  49. package/dist/index.d.ts +6 -4
  50. package/dist/index.js +10 -7
  51. package/dist/ink-string.d.ts +23 -0
  52. package/dist/ink-string.js +65 -0
  53. package/dist/interactive.d.ts +66 -0
  54. package/dist/interactive.js +207 -0
  55. package/dist/layout.js +1 -2
  56. package/dist/pack-loader.d.ts +64 -0
  57. package/dist/pack-loader.js +172 -0
  58. package/dist/pack-script.d.ts +44 -0
  59. package/dist/pack-script.js +135 -0
  60. package/dist/registry.d.ts +106 -100
  61. package/dist/registry.js +17 -9
  62. package/dist/render.d.ts +63 -25
  63. package/dist/render.js +1001 -380
  64. package/dist/text-grid.d.ts +90 -0
  65. package/dist/text-grid.js +300 -0
  66. package/package.json +9 -5
  67. package/dist/box.d.ts +0 -47
  68. package/dist/box.js +0 -209
  69. package/dist/measure.d.ts +0 -25
  70. package/dist/measure.js +0 -108
@@ -1,14 +1,12 @@
1
1
  import { layoutWrapperAxis } from '@markii/stdlib';
2
2
  import { applyLayout } from '../layout.js';
3
+ import { childrenText } from '../registry.js';
3
4
  /**
4
5
  * The closed set of layout-wrapper container names (docs/format.md):
5
- * aliases of the one shared implementation below (`createLayoutWrapper`),
6
- * matching `@markii/html`'s `layout-wrapper.ts`. There is deliberately no
7
- * `normal` alias: the default needs no wrapper at all. Every name here is
8
- * ALSO one of `@markii/stdlib`'s own width/align preset values (`center`/
9
- * `left`/`right` are align presets, `wide`/`narrow`/`full`/`fit` are width
10
- * presets), which is what lets `createLayoutWrapper` use the preset name
11
- * itself as that axis's value with no separate lookup table.
6
+ * aliases of the one shared implementation below (`createLayoutWrapper`).
7
+ * There is deliberately no `normal` alias: the default needs no wrapper at
8
+ * all. Every name here is ALSO one of `@markii/stdlib`'s own width/align
9
+ * preset values.
12
10
  */
13
11
  export const LAYOUT_WRAPPER_PRESETS = [
14
12
  'center',
@@ -22,20 +20,8 @@ export const LAYOUT_WRAPPER_PRESETS = [
22
20
  /**
23
21
  * Creates the registry component for one of docs/format.md's layout-wrapper
24
22
  * container names. One shared implementation, bound to `preset` at
25
- * registration time, matching `@markii/html`'s `createLayoutWrapper` in
26
- * spirit: it never reads `attributes` at all — `render.ts` already stripped
27
- * both reserved keys before this ever runs.
28
- *
29
- * A wrapper sets ONE axis by its own NAME (docs/spec.md §3): `preset` itself
30
- * IS that axis's value (`center` sets `align: 'center'`; `narrow` sets
31
- * `width: 'narrow'`), so it is applied unconditionally, whatever the author
32
- * wrote for that axis's own reserved attribute (already discarded — the
33
- * name always wins). The OTHER axis, when the author supplied it, arrives
34
- * as `ctx.layout` (`render.ts` resolved it on this wrapper's behalf, since
35
- * this wrapper is the directive's registered `layout` scope). Both are
36
- * merged into one `ResolvedLayoutPresets` and applied together via
37
- * `../layout.js`'s `applyLayout`, so `:::center{width=fit}` narrows AND
38
- * centers in one pass.
23
+ * registration time: it never reads `attributes` at all — `render.tsx`
24
+ * already stripped both reserved keys before this ever runs.
39
25
  */
40
26
  export function createLayoutWrapper(preset) {
41
27
  const ownAxis = layoutWrapperAxis(preset);
@@ -46,9 +32,9 @@ export function createLayoutWrapper(preset) {
46
32
  const own = ownAxis === 'align'
47
33
  ? { align: preset }
48
34
  : { width: preset };
49
- return (_attributes, children, ctx) => {
35
+ return ({ children, ctx }) => {
50
36
  const merged = { ...own, ...ctx.layout };
51
- return applyLayout(children(), merged, ctx.width);
37
+ return applyLayout(childrenText(children), merged, ctx.width);
52
38
  };
53
39
  }
54
40
  /** The layout axis `preset` sets by its own name. Mirrors `@markii/html`'s `layoutWrapperPresetAxis`. */
@@ -1,15 +1,7 @@
1
1
  import type { AnsiComponent } from '../registry.js';
2
2
  /**
3
- * `::progress{value=3 max=5 label="tasks"}` — a meter bar. Data binding (§8)
4
- * mirrors `@markii/html`'s `Progress`: a bound number supplies `value`; a
5
- * bound object may supply `value`/`max` — explicit attributes always win.
6
- * Non-numeric/`NaN`/`Infinity` input falls back to `0` (value) or the
7
- * default `max` of `1`; the effective value is clamped to `[0, max]`.
8
- * Terminal form: `label bar percent`, the bar drawn with `█` (filled) and
9
- * `░` (empty) at up to `MAX_BAR_WIDTH` columns, narrower when the label and
10
- * percent text leave less room. `format`/`decimals`, when given, format the
11
- * `value/max` fraction for the percent readout in place of the default
12
- * rounded integer percent, matching `@markii/html` exactly. A failed/stale
13
- * binding appends the quiet failure suffix after the percent.
3
+ * `::progress{value=3 max=5 label="tasks"}` — a meter bar. Data binding
4
+ * (§8): a bound number supplies `value`; a bound object may supply
5
+ * `value`/`max` — explicit attributes always win.
14
6
  */
15
7
  export declare const Progress: AnsiComponent;
@@ -1,5 +1,5 @@
1
1
  import { formatValue } from '@markii/stdlib';
2
- import { measure } from '../measure.js';
2
+ import { measureWidth } from '../text-grid.js';
3
3
  import { safeRead } from '../resolve.js';
4
4
  import { dataStateSuffix, failureToken } from '../failure-presentation.js';
5
5
  const DEFAULT_MAX = 1;
@@ -33,24 +33,14 @@ function readProgressFields(data) {
33
33
  return {};
34
34
  }
35
35
  /** Block characters the bar fills with, and its widest allowed footprint (columns), sized down further when the label/percent text leaves less room. */
36
- const FILLED_CHAR = '█';
37
- const EMPTY_CHAR = '░';
38
36
  const MAX_BAR_WIDTH = 30;
39
37
  const MIN_BAR_WIDTH = 3;
40
38
  /**
41
- * `::progress{value=3 max=5 label="tasks"}` — a meter bar. Data binding (§8)
42
- * mirrors `@markii/html`'s `Progress`: a bound number supplies `value`; a
43
- * bound object may supply `value`/`max` — explicit attributes always win.
44
- * Non-numeric/`NaN`/`Infinity` input falls back to `0` (value) or the
45
- * default `max` of `1`; the effective value is clamped to `[0, max]`.
46
- * Terminal form: `label bar percent`, the bar drawn with `█` (filled) and
47
- * `░` (empty) at up to `MAX_BAR_WIDTH` columns, narrower when the label and
48
- * percent text leave less room. `format`/`decimals`, when given, format the
49
- * `value/max` fraction for the percent readout in place of the default
50
- * rounded integer percent, matching `@markii/html` exactly. A failed/stale
51
- * binding appends the quiet failure suffix after the percent.
39
+ * `::progress{value=3 max=5 label="tasks"}` — a meter bar. Data binding
40
+ * (§8): a bound number supplies `value`; a bound object may supply
41
+ * `value`/`max` — explicit attributes always win.
52
42
  */
53
- export const Progress = (attributes, _children, ctx) => {
43
+ export const Progress = ({ attributes, ctx }) => {
54
44
  const { data, dataStatus, dataFailureKind } = ctx;
55
45
  const bound = safeRead(() => dataStatus === 'missing' || dataStatus === 'error'
56
46
  ? {}
@@ -70,9 +60,10 @@ export const Progress = (attributes, _children, ctx) => {
70
60
  const suffix = dataStateSuffix(dataStatus, dataFailureKind);
71
61
  const token = failureToken(dataFailureKind);
72
62
  const percentPart = ` ${ctx.text(percentText)}${suffix ? (token ? ctx.style(suffix, token) : ctx.dim(suffix)) : ''}`;
73
- const overhead = measure(labelPart) + measure(percentText) + 1;
63
+ const overhead = measureWidth(labelPart) + measureWidth(percentText) + 1;
74
64
  const barWidth = Math.max(MIN_BAR_WIDTH, Math.min(MAX_BAR_WIDTH, ctx.width - overhead));
75
65
  const filledCount = Math.round((percent / 100) * barWidth);
76
- const bar = FILLED_CHAR.repeat(filledCount) + EMPTY_CHAR.repeat(barWidth - filledCount);
66
+ const bar = ctx.glyphs.barFilled.repeat(filledCount) +
67
+ ctx.glyphs.barEmpty.repeat(barWidth - filledCount);
77
68
  return `${labelPart}${bar}${percentPart}`;
78
69
  };
@@ -3,7 +3,6 @@ import type { AnsiComponent } from '../registry.js';
3
3
  * `::rating{value=3 max=5}` — a leaf directive rendering a row of stars.
4
4
  * Both attributes are optional and clamped to sane bounds; malformed input
5
5
  * degrades gracefully instead of throwing. Terminal form: filled (`★`) and
6
- * empty (`☆`) stars, one per position up to `max`. Matches
7
- * `@markii/html`'s `Rating` clamping rules exactly.
6
+ * empty (`☆`) stars, one per position up to `max`.
8
7
  */
9
8
  export declare const Rating: AnsiComponent;
@@ -14,15 +14,14 @@ function clamp(value, min, max) {
14
14
  * `::rating{value=3 max=5}` — a leaf directive rendering a row of stars.
15
15
  * Both attributes are optional and clamped to sane bounds; malformed input
16
16
  * degrades gracefully instead of throwing. Terminal form: filled (`★`) and
17
- * empty (`☆`) stars, one per position up to `max`. Matches
18
- * `@markii/html`'s `Rating` clamping rules exactly.
17
+ * empty (`☆`) stars, one per position up to `max`.
19
18
  */
20
- export const Rating = (attributes) => {
19
+ export const Rating = ({ attributes, ctx }) => {
21
20
  const max = clamp(parseCount(attributes.max, DEFAULT_MAX), MIN_MAX, MAX_MAX);
22
21
  const value = clamp(parseCount(attributes.value, 0), 0, max);
23
22
  let stars = '';
24
23
  for (let index = 0; index < max; index += 1) {
25
- stars += index < value ? '★' : '☆';
24
+ stars += index < value ? ctx.glyphs.starFilled : ctx.glyphs.starEmpty;
26
25
  }
27
26
  return stars;
28
27
  };
@@ -1,26 +1,41 @@
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
+ /** The blank columns between two cells of a grid row. Shared by both emitters (the Ink flex `Box` and the flattened string). */
7
+ export declare const ROW_GUTTER = 1;
8
+ export interface RowLayout {
9
+ align: RowTextAlign;
10
+ columnCount: number;
11
+ colWidth: number;
12
+ stacked: boolean;
13
+ }
4
14
  /**
5
- * `:::row{cols=2|3|4 text=left|center|right} ... :::` — docs/format.md's one
6
- * layout container. An absent/invalid `cols` auto-fits: every direct block
7
- * child becomes one column, all in a single row of columns. A `cols` value
8
- * smaller than the child count wraps into further grid rows, `cols` cells at
9
- * a time — the terminal counterpart of the CSS grid wrap a live host does.
10
- * Below `ROW_COLUMN_THRESHOLD` columns, or with only one cell, cells stack
11
- * vertically instead (a plain blank-line-separated list) — a fixed-width
12
- * terminal has no responsive reflow, so this is the one width breakpoint
13
- * that stands in for it.
15
+ * `:::row{cols=2|3|4 text=left|center|right} ... :::`'s pure layout
16
+ * decision: how many columns, how wide each one is, and whether the row
17
+ * stacks. `render.tsx`'s walk calls this directly (rather than `row`'s own
18
+ * registered component) so it can build each cell's real Ink subtree at the
19
+ * correct width BEFORE assembling the row's flex `Box` — see `render.tsx`'s
20
+ * top comment and `registry.ts`'s doc comment on why this one directive is
21
+ * handled at the walk level. Kept here, not in `render.tsx`, so
22
+ * `components/contract-drift.test.ts`'s literal source scan of `row.tsx`
23
+ * still finds the `attributes.cols`/`attributes.text` reads.
14
24
  *
15
- * Each cell is one of the directive's own top-level children
16
- * (`registry.ts`'s `AnsiChildren.parts`: a `:::cell` directive grouping
17
- * several blocks, or any other block standing for itself, per docs/format.md's
18
- * "a row counts its direct block children as its cells"), rendered through
19
- * that part's own `render({ width })` at the ACTUAL column width decided
20
- * below, rather than rendered once at the row's full width and then
21
- * re-wrapped into a narrower column. A cell containing a self-drawing box
22
- * (a nested `card`/`table`) therefore draws that box at the real column
23
- * width in the first place, instead of having an already-drawn frame
24
- * mangled by a later re-wrap.
25
+ * An absent/invalid `cols` auto-fits: every cell becomes one column, all in
26
+ * a single grid row. A `cols` value smaller than the cell count wraps into
27
+ * further grid rows, `cols` cells at a time. Below `ROW_COLUMN_THRESHOLD`
28
+ * columns, or with only one cell, cells stack vertically instead.
29
+ */
30
+ export declare function resolveRowLayout(attributes: DirectiveAttributes, cellCount: number, width: number): RowLayout;
31
+ /**
32
+ * `row`'s registered component: a passthrough. `render.tsx`'s walk builds
33
+ * the row's REAL flex-`Box` grid (or, for a `row` reached through STRING
34
+ * mode nested inside a self-drawing container, a flattened string) directly
35
+ * and hands it down as `children`, so the registered component's own job is
36
+ * just to return what it was given. Kept as a real registration (rather
37
+ * than omitted) so alias resolution, the contract-drift coverage test, and
38
+ * an unexpected direct invocation all still see a real `row` component.
25
39
  */
26
40
  export declare const Row: AnsiComponent;
41
+ export {};
@@ -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,44 @@ 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;
11
+ /** The blank columns between two cells of a grid row. Shared by both emitters (the Ink flex `Box` and the flattened string). */
12
+ export const ROW_GUTTER = 1;
14
13
  /**
15
- * `:::row{cols=2|3|4 text=left|center|right} ... :::` — docs/format.md's one
16
- * layout container. An absent/invalid `cols` auto-fits: every direct block
17
- * child becomes one column, all in a single row of columns. A `cols` value
18
- * smaller than the child count wraps into further grid rows, `cols` cells at
19
- * a time — the terminal counterpart of the CSS grid wrap a live host does.
20
- * Below `ROW_COLUMN_THRESHOLD` columns, or with only one cell, cells stack
21
- * vertically instead (a plain blank-line-separated list) — a fixed-width
22
- * terminal has no responsive reflow, so this is the one width breakpoint
23
- * that stands in for it.
14
+ * `:::row{cols=2|3|4 text=left|center|right} ... :::`'s pure layout
15
+ * decision: how many columns, how wide each one is, and whether the row
16
+ * stacks. `render.tsx`'s walk calls this directly (rather than `row`'s own
17
+ * registered component) so it can build each cell's real Ink subtree at the
18
+ * correct width BEFORE assembling the row's flex `Box` — see `render.tsx`'s
19
+ * top comment and `registry.ts`'s doc comment on why this one directive is
20
+ * handled at the walk level. Kept here, not in `render.tsx`, so
21
+ * `components/contract-drift.test.ts`'s literal source scan of `row.tsx`
22
+ * still finds the `attributes.cols`/`attributes.text` reads.
24
23
  *
25
- * Each cell is one of the directive's own top-level children
26
- * (`registry.ts`'s `AnsiChildren.parts`: a `:::cell` directive grouping
27
- * several blocks, or any other block standing for itself, per docs/format.md's
28
- * "a row counts its direct block children as its cells"), rendered through
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.
24
+ * An absent/invalid `cols` auto-fits: every cell becomes one column, all in
25
+ * a single grid row. A `cols` value smaller than the cell count wraps into
26
+ * further grid rows, `cols` cells at a time. Below `ROW_COLUMN_THRESHOLD`
27
+ * columns, or with only one cell, cells stack vertically instead.
35
28
  */
36
- export const Row = (attributes, children, ctx) => {
29
+ export function resolveRowLayout(attributes, cellCount, width) {
37
30
  const rawTextAlign = attributes.text;
38
31
  const align = rawTextAlign && isTextAlign(rawTextAlign) ? rawTextAlign : 'left';
39
- const cellParts = children.parts;
40
- if (cellParts.length === 0)
41
- return '';
42
32
  const rawCols = attributes.cols ?? '';
43
- const requestedCols = isColsValue(rawCols)
44
- ? Number(rawCols)
45
- : cellParts.length;
46
- const columnCount = Math.max(1, Math.min(requestedCols, cellParts.length));
47
- const place = (text, width) => align === 'left'
48
- ? text
49
- : text
50
- .split('\n')
51
- .map((line) => ctx.pad(line, width, align))
52
- .join('\n');
53
- if (ctx.width < ROW_COLUMN_THRESHOLD || columnCount <= 1) {
54
- return cellParts
55
- .map((part) => place(part.render({ width: ctx.width }), ctx.width))
56
- .join('\n\n');
57
- }
58
- const colWidth = Math.max(1, Math.floor((ctx.width - GUTTER * (columnCount - 1)) / columnCount));
59
- const placedCells = cellParts.map((part) => place(part.render({ width: colWidth }), colWidth));
60
- const gridRows = [];
61
- for (let index = 0; index < placedCells.length; index += columnCount) {
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
- };
33
+ const requestedCols = isColsValue(rawCols) ? Number(rawCols) : cellCount;
34
+ const columnCount = Math.max(1, Math.min(requestedCols, Math.max(1, cellCount)));
35
+ const gutter = ROW_GUTTER;
36
+ const stacked = width < ROW_COLUMN_THRESHOLD || columnCount <= 1;
37
+ const colWidth = stacked
38
+ ? width
39
+ : Math.max(1, Math.floor((width - gutter * (columnCount - 1)) / columnCount));
40
+ return { align, columnCount, colWidth, stacked };
41
+ }
42
+ /**
43
+ * `row`'s registered component: a passthrough. `render.tsx`'s walk builds
44
+ * the row's REAL flex-`Box` grid (or, for a `row` reached through STRING
45
+ * mode nested inside a self-drawing container, a flattened string) directly
46
+ * and hands it down as `children`, so the registered component's own job is
47
+ * just to return what it was given. Kept as a real registration (rather
48
+ * than omitted) so alias resolution, the contract-drift coverage test, and
49
+ * an unexpected direct invocation all still see a real `row` component.
50
+ */
51
+ 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) mirrors `@markii/html`'s `Stat` exactly: a bound number/string
5
- * supplies `value`; a bound object may supply `value`/`label`/`delta`/`trend`
6
- * — an explicit attribute always wins. Missing value renders `—` rather than
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;
@@ -1,7 +1,6 @@
1
1
  import { formatValue } from '@markii/stdlib';
2
2
  import { safeRead } from '../resolve.js';
3
3
  import { dataStateSuffix, failureToken } from '../failure-presentation.js';
4
- const EMPTY_VALUE = '—';
5
4
  const TRENDS = ['up', 'down', 'flat'];
6
5
  function isTrend(value) {
7
6
  return TRENDS.includes(value);
@@ -35,16 +34,11 @@ function pick(attribute, fromData) {
35
34
  }
36
35
  /**
37
36
  * `::stat{value=42 label="stars" trend=up}` — a big value plus label. Data
38
- * binding (§8) mirrors `@markii/html`'s `Stat` exactly: a bound number/string
39
- * supplies `value`; a bound object may supply `value`/`label`/`delta`/`trend`
40
- * — an explicit attribute always wins. Missing value renders `—` rather than
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").
37
+ * binding (§8): a bound number/string supplies `value`; a bound object may
38
+ * supply `value`/`label`/`delta`/`trend` — an explicit attribute always
39
+ * wins. Missing value renders `—` rather than a blank line.
46
40
  */
47
- export const Stat = (attributes, _children, ctx) => {
41
+ export const Stat = ({ attributes, ctx }) => {
48
42
  const { data, dataStatus, dataFailureKind } = ctx;
49
43
  const bound = safeRead(() => dataStatus === 'missing' || dataStatus === 'error'
50
44
  ? {}
@@ -59,7 +53,7 @@ export const Stat = (attributes, _children, ctx) => {
59
53
  const lines = [];
60
54
  if (label)
61
55
  lines.push(ctx.style(ctx.text(label), '--mk-muted'));
62
- let valueLine = ctx.bold(ctx.text(formattedValue || EMPTY_VALUE));
56
+ let valueLine = ctx.bold(ctx.text(formattedValue || ctx.glyphs.emptyValue));
63
57
  if (delta) {
64
58
  const deltaToken = trend === 'up'
65
59
  ? '--mk-success'
@@ -1,13 +1,14 @@
1
- import type { AnsiComponent } from '../registry.js';
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.ts`). Unlike `@markii/html`'s `Tab` (which never reads `label` at
6
- * all — see that module's `tabs.ts` for why a string-based engine usually
7
- * can't reach a child directive's own attributes from its parent), THIS
8
- * engine's `tab` reads its OWN `label` directly, since it is rendered as
9
- * its OWN directive, not inspected by `tabs` from the outside: the label
10
- * becomes a bold heading line above the panel body. Rendered standalone
11
- * (outside a `tabs` parent) it shows exactly the same heading and panel.
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;
@@ -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.ts`). Unlike `@markii/html`'s `Tab` (which never reads `label` at
5
- * all — see that module's `tabs.ts` for why a string-based engine usually
6
- * can't reach a child directive's own attributes from its parent), THIS
7
- * engine's `tab` reads its OWN `label` directly, since it is rendered as
8
- * its OWN directive, not inspected by `tabs` from the outside: the label
9
- * becomes a bold heading line above the panel body. Rendered standalone
10
- * (outside a `tabs` parent) it shows exactly the same heading and panel.
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 childrenText = children();
16
- return childrenText ? `${heading}\n${childrenText}` : heading;
17
+ const body = childrenText(children);
18
+ return body ? `${heading}\n${body}` : heading;
17
19
  };
@@ -1,3 +1,4 @@
1
+ import type { GlyphSet } from '../glyphs.js';
1
2
  /**
2
3
  * The ONE box-drawing/width-negotiation routine every table this engine
3
4
  * draws goes through: the data-bound `::table` component (`./table.ts`) and
@@ -29,6 +30,6 @@ export declare const MIN_COLUMN_WIDTH = 3;
29
30
  * complete grid at most `width` columns wide when the natural content
30
31
  * allows it, otherwise as narrow as `MIN_COLUMN_WIDTH` per column permits.
31
32
  */
32
- export declare function drawTableGrid(header: readonly string[] | undefined, rows: readonly (readonly string[])[], width: number, boldFn?: (text: string) => string, align?: 'left' | 'center' | 'right'): string;
33
+ export declare function drawTableGrid(header: readonly string[] | undefined, rows: readonly (readonly string[])[], width: number, glyphs: GlyphSet, boldFn?: (text: string) => string, align?: 'left' | 'center' | 'right'): string;
33
34
  /** The widest a `drawTableGrid` result's lines get, for a `selfLayout` component (`./table.ts`) that needs to know its own natural footprint (the `fit` width preset). */
34
35
  export declare function measureTableGridWidth(header: readonly string[] | undefined, rows: readonly (readonly string[])[]): number;
@@ -1,5 +1,4 @@
1
- import { pad, wrap } from '../box.js';
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
@@ -76,7 +75,7 @@ function wrapRow(row, widths) {
76
75
  const height = Math.max(1, ...lines.map((l) => l.length));
77
76
  return { lines, height };
78
77
  }
79
- function drawRow(row, widths, boldRow, boldFn, align) {
78
+ function drawRow(row, widths, boldRow, boldFn, align, glyphs) {
80
79
  const { lines, height } = wrapRow(row, widths);
81
80
  const rowLines = [];
82
81
  for (let line = 0; line < height; line += 1) {
@@ -84,12 +83,14 @@ function drawRow(row, widths, boldRow, boldFn, align) {
84
83
  const text = pad(lines[c]?.[line] ?? '', w, align);
85
84
  return boldRow ? boldFn(text) : text;
86
85
  });
87
- rowLines.push(`│ ${cells.join(' │ ')} │`);
86
+ const bar = glyphs.solid.vertical;
87
+ rowLines.push(`${bar} ${cells.join(` ${bar} `)} ${bar}`);
88
88
  }
89
89
  return rowLines.join('\n');
90
90
  }
91
- function drawSeparator(widths, left, mid, right) {
92
- return `${left}${widths.map((w) => '─'.repeat(w + CELL_PADDING)).join(mid)}${right}`;
91
+ function drawSeparator(widths, left, mid, right, glyphs) {
92
+ const rule = glyphs.solid.horizontal;
93
+ return `${left}${widths.map((w) => rule.repeat(w + CELL_PADDING)).join(mid)}${right}`;
93
94
  }
94
95
  /**
95
96
  * Draws a box-drawn table. `header`, when given, is the bold first row,
@@ -101,19 +102,20 @@ function drawSeparator(widths, left, mid, right) {
101
102
  * complete grid at most `width` columns wide when the natural content
102
103
  * allows it, otherwise as narrow as `MIN_COLUMN_WIDTH` per column permits.
103
104
  */
104
- export function drawTableGrid(header, rows, width, boldFn = (text) => text, align = 'left') {
105
+ export function drawTableGrid(header, rows, width, glyphs, boldFn = (text) => text, align = 'left') {
105
106
  const columnCount = Math.max(header?.length ?? 0, ...rows.map((row) => row.length), 1);
106
107
  const natural = naturalColumnWidths(header, rows, columnCount);
107
108
  const widths = negotiateColumnWidths(natural, width);
108
109
  const lines = [];
109
- lines.push(drawSeparator(widths, '┌', '┬', '┐'));
110
+ const g = glyphs;
111
+ lines.push(drawSeparator(widths, g.topLeft, g.teeDown, g.topRight, g));
110
112
  if (header) {
111
- lines.push(drawRow(header, widths, true, boldFn, align));
112
- lines.push(drawSeparator(widths, '├', '┼', '┤'));
113
+ lines.push(drawRow(header, widths, true, boldFn, align, g));
114
+ lines.push(drawSeparator(widths, g.teeRight, g.cross, g.teeLeft, g));
113
115
  }
114
116
  for (const row of rows)
115
- lines.push(drawRow(row, widths, false, boldFn, align));
116
- lines.push(drawSeparator(widths, '└', '┴', '┘'));
117
+ lines.push(drawRow(row, widths, false, boldFn, align, g));
118
+ lines.push(drawSeparator(widths, g.bottomLeft, g.teeUp, g.bottomRight, g));
117
119
  return lines.join('\n');
118
120
  }
119
121
  /** The widest a `drawTableGrid` result's lines get, for a `selfLayout` component (`./table.ts`) that needs to know its own natural footprint (the `fit` width preset). */
@@ -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) mirrors `@markii/html`'s `Table`: `@markii/stdlib`'s
5
- * `deriveTableShape` decides the layout from the bound value's own shape.
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` (see `card.ts`'s doc comment): draws real
12
- * box-drawing glyphs (`./table-grid.ts`), so it sizes its own grid off
13
- * `ctx.layout` instead of letting a generic post-render narrow/pad corrupt
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;
@@ -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, matching `@markii/html`'s `Table` exactly. */
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) mirrors `@markii/html`'s `Table`: `@markii/stdlib`'s
40
- * `deriveTableShape` decides the layout from the bound value's own shape.
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` (see `card.ts`'s doc comment): draws real
47
- * box-drawing glyphs (`./table-grid.ts`), so it sizes its own grid off
48
- * `ctx.layout` instead of letting a generic post-render narrow/pad corrupt
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, _children, ctx) => {
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);
@@ -94,7 +89,7 @@ export const Table = (attributes, _children, ctx) => {
94
89
  }
95
90
  const naturalWidth = measureTableGridWidth(header, rows);
96
91
  const boxWidth = selfLayoutWidth(ctx.layout, ctx.width, naturalWidth);
97
- const grid = drawTableGrid(header, rows, boxWidth, (text) => ctx.bold(text), align);
92
+ const grid = drawTableGrid(header, rows, boxWidth, ctx.glyphs, (text) => ctx.bold(text), align);
98
93
  const body = `${grid}${styledSuffix}`;
99
94
  const withCaption = captionLine ? `${captionLine}\n${body}` : body;
100
95
  return selfLayoutAlign(withCaption, ctx.layout, ctx.width);
@@ -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;