@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.
Files changed (60) hide show
  1. package/dist/ansi.d.ts +26 -0
  2. package/dist/ansi.js +51 -0
  3. package/dist/components/badge.d.ts +1 -1
  4. package/dist/components/badge.js +5 -4
  5. package/dist/components/callout.d.ts +24 -13
  6. package/dist/components/callout.js +45 -21
  7. package/dist/components/card.d.ts +24 -2
  8. package/dist/components/card.js +41 -12
  9. package/dist/components/cell.d.ts +5 -11
  10. package/dist/components/cell.js +11 -15
  11. package/dist/components/chart.d.ts +4 -10
  12. package/dist/components/chart.js +9 -16
  13. package/dist/components/details-string.d.ts +26 -0
  14. package/dist/components/details-string.js +39 -0
  15. package/dist/components/details.d.ts +30 -10
  16. package/dist/components/details.js +27 -25
  17. package/dist/components/divider.js +9 -11
  18. package/dist/components/figure.d.ts +5 -16
  19. package/dist/components/figure.js +9 -19
  20. package/dist/components/index.d.ts +14 -10
  21. package/dist/components/index.js +19 -16
  22. package/dist/components/kbd.d.ts +1 -1
  23. package/dist/components/kbd.js +3 -2
  24. package/dist/components/layout-wrapper.d.ts +7 -22
  25. package/dist/components/layout-wrapper.js +9 -23
  26. package/dist/components/progress.d.ts +3 -11
  27. package/dist/components/progress.js +6 -14
  28. package/dist/components/rating.d.ts +1 -2
  29. package/dist/components/rating.js +2 -3
  30. package/dist/components/row.d.ts +33 -20
  31. package/dist/components/row.js +33 -51
  32. package/dist/components/stat.d.ts +3 -8
  33. package/dist/components/stat.js +4 -9
  34. package/dist/components/tab.d.ts +9 -8
  35. package/dist/components/tab.js +12 -10
  36. package/dist/components/table-grid.js +1 -2
  37. package/dist/components/table.d.ts +5 -10
  38. package/dist/components/table.js +7 -12
  39. package/dist/components/tabs-string.d.ts +17 -0
  40. package/dist/components/tabs-string.js +30 -0
  41. package/dist/components/tabs.d.ts +29 -14
  42. package/dist/components/tabs.js +37 -30
  43. package/dist/index.d.ts +2 -4
  44. package/dist/index.js +6 -7
  45. package/dist/ink-string.d.ts +12 -0
  46. package/dist/ink-string.js +41 -0
  47. package/dist/interactive.d.ts +41 -0
  48. package/dist/interactive.js +64 -0
  49. package/dist/layout.js +1 -2
  50. package/dist/registry.d.ts +91 -100
  51. package/dist/registry.js +17 -9
  52. package/dist/render.d.ts +39 -24
  53. package/dist/render.js +676 -373
  54. package/dist/text-grid.d.ts +54 -0
  55. package/dist/text-grid.js +249 -0
  56. package/package.json +7 -4
  57. package/dist/box.d.ts +0 -47
  58. package/dist/box.js +0 -209
  59. package/dist/measure.d.ts +0 -25
  60. package/dist/measure.js +0 -108
package/dist/ansi.d.ts CHANGED
@@ -39,6 +39,32 @@ export declare const italic: (text: string, level: ColorLevel) => string;
39
39
  export declare const underline: (text: string, level: ColorLevel) => string;
40
40
  /** SGR inverse/reverse video (7). Returns `text` unchanged at `'none'`. */
41
41
  export declare const inverse: (text: string, level: ColorLevel) => string;
42
+ /**
43
+ * Repairs an SGR escape span that would otherwise cross an embedded `\n`
44
+ * inside `text`, so every physical LINE is independently self-contained:
45
+ * whatever SGR codes are still "open" (emitted since the last full reset)
46
+ * at a line boundary are closed with a reset before the `\n` and reopened
47
+ * at the start of the next line.
48
+ *
49
+ * This is the SYSTEMATIC fix for a real Ink behavior discovered during
50
+ * batch 10's post-phase-2 review: Ink's `<Text>` does not treat an embedded
51
+ * raw ANSI escape as transparent across an embedded newline the way a
52
+ * terminal receiving the same bytes directly would — it resets SGR state
53
+ * at every line boundary within one `<Text>` value and does not reopen it.
54
+ * Every multi-line string this engine hands to a single `<Text>` node
55
+ * (`render.tsx`'s `styledText` helper) MUST pass through this function
56
+ * first: a dim-wrapped multi-line frame, a bold-and-underlined heading that
57
+ * wrapped onto more than one line, a paragraph whose bold span happened to
58
+ * straddle a wrap point — anything this engine itself line-wrapped (with
59
+ * `text-grid.ts`'s `wrapText`) while carrying an open style. It is a no-op,
60
+ * fast-pathed, for a string with no `\n` at all.
61
+ *
62
+ * Every SGR code this engine's own helpers ever emit closes with a FULL
63
+ * reset (`\x1b[0m`), never a narrow one (`ansi.ts`'s `wrapSgr`), so
64
+ * tracking "what is currently open" only ever needs a flat list cleared by
65
+ * the next `\x1b[0m` (or a bare `\x1b[m`) — never per-attribute bookkeeping.
66
+ */
67
+ export declare function carrySgrAcrossLines(text: string): string;
42
68
  /**
43
69
  * OSC 8: `ESC ] 8 ; ; url BEL text ESC ] 8 ; ; BEL`. Emitted only when
44
70
  * `level` is not `'none'`; at `'none'` this returns `text` UNCHANGED, and
package/dist/ansi.js CHANGED
@@ -63,6 +63,57 @@ export const italic = sgrWrap(3);
63
63
  export const underline = sgrWrap(4);
64
64
  /** SGR inverse/reverse video (7). Returns `text` unchanged at `'none'`. */
65
65
  export const inverse = sgrWrap(7);
66
+ /** Matches one SGR sequence, capturing its raw parameter string (empty for a bare `ESC [ m`). */
67
+ const SGR_TOKEN = /\x1b\[([0-9;]*)m/g;
68
+ /**
69
+ * Repairs an SGR escape span that would otherwise cross an embedded `\n`
70
+ * inside `text`, so every physical LINE is independently self-contained:
71
+ * whatever SGR codes are still "open" (emitted since the last full reset)
72
+ * at a line boundary are closed with a reset before the `\n` and reopened
73
+ * at the start of the next line.
74
+ *
75
+ * This is the SYSTEMATIC fix for a real Ink behavior discovered during
76
+ * batch 10's post-phase-2 review: Ink's `<Text>` does not treat an embedded
77
+ * raw ANSI escape as transparent across an embedded newline the way a
78
+ * terminal receiving the same bytes directly would — it resets SGR state
79
+ * at every line boundary within one `<Text>` value and does not reopen it.
80
+ * Every multi-line string this engine hands to a single `<Text>` node
81
+ * (`render.tsx`'s `styledText` helper) MUST pass through this function
82
+ * first: a dim-wrapped multi-line frame, a bold-and-underlined heading that
83
+ * wrapped onto more than one line, a paragraph whose bold span happened to
84
+ * straddle a wrap point — anything this engine itself line-wrapped (with
85
+ * `text-grid.ts`'s `wrapText`) while carrying an open style. It is a no-op,
86
+ * fast-pathed, for a string with no `\n` at all.
87
+ *
88
+ * Every SGR code this engine's own helpers ever emit closes with a FULL
89
+ * reset (`\x1b[0m`), never a narrow one (`ansi.ts`'s `wrapSgr`), so
90
+ * tracking "what is currently open" only ever needs a flat list cleared by
91
+ * the next `\x1b[0m` (or a bare `\x1b[m`) — never per-attribute bookkeeping.
92
+ */
93
+ export function carrySgrAcrossLines(text) {
94
+ if (!text.includes('\n'))
95
+ return text;
96
+ const lines = text.split('\n');
97
+ let openCodes = [];
98
+ const result = [];
99
+ for (const line of lines) {
100
+ const prefix = openCodes.map((params) => `${ESC}[${params}m`).join('');
101
+ SGR_TOKEN.lastIndex = 0;
102
+ let match;
103
+ while ((match = SGR_TOKEN.exec(line)) !== null) {
104
+ const params = match[1] ?? '';
105
+ if (params === '' || params === '0') {
106
+ openCodes = [];
107
+ }
108
+ else {
109
+ openCodes = [...openCodes, params];
110
+ }
111
+ }
112
+ const suffix = openCodes.length > 0 ? RESET : '';
113
+ result.push(`${prefix}${line}${suffix}`);
114
+ }
115
+ return result.join('\n');
116
+ }
66
117
  /**
67
118
  * OSC 8: `ESC ] 8 ; ; url BEL text ESC ] 8 ; ; BEL`. Emitted only when
68
119
  * `level` is not `'none'`; at `'none'` this returns `text` UNCHANGED, and
@@ -1,4 +1,4 @@
1
- import type { AnsiComponent } from '../registry.js';
1
+ import { type AnsiComponent } from '../registry.js';
2
2
  export type BadgeVariant = 'neutral' | 'info' | 'success' | 'warning' | 'danger';
3
3
  /**
4
4
  * `:badge[New]{variant=success}` — a status pill for an inline text
@@ -1,3 +1,4 @@
1
+ import { childrenText } from '../registry.js';
1
2
  const BADGE_VARIANTS = [
2
3
  'neutral',
3
4
  'info',
@@ -31,14 +32,14 @@ const TOKEN_NAME = {
31
32
  * variant. At color level `'none'` there is no inverse video to distinguish
32
33
  * a badge from surrounding text, so it degrades to `[label]` instead.
33
34
  */
34
- export const Badge = (attributes, children, ctx) => {
35
+ export const Badge = ({ attributes, children, ctx }) => {
35
36
  const rawVariant = attributes.variant ?? DEFAULT_VARIANT;
36
37
  const variant = isBadgeVariant(rawVariant)
37
38
  ? rawVariant
38
39
  : DEFAULT_VARIANT;
39
- const childrenText = children();
40
+ const text = childrenText(children);
40
41
  if (ctx.color === 'none')
41
- return `[${childrenText}]`;
42
+ return `[${text}]`;
42
43
  const token = TOKEN_NAME[VARIANT_TOKEN[variant]];
43
- return ctx.inverse(ctx.style(` ${childrenText} `, token));
44
+ return ctx.inverse(ctx.style(` ${text} `, token));
44
45
  };
@@ -1,20 +1,31 @@
1
- import type { AnsiComponent } from '../registry.js';
1
+ import type { ResolvedLayoutPresets } from '../layout.js';
2
+ import { type AnsiComponent, type DirectiveAttributes } from '../registry.js';
2
3
  export type CalloutType = 'info' | 'warning' | 'danger';
4
+ /**
5
+ * The inner budget `callout`'s body renders at, given ONLY its own
6
+ * attributes and the outer width — never the body itself. Mirrors
7
+ * `card.ts`'s `resolveCardInnerWidth`: `render.tsx`'s walk calls this
8
+ * BEFORE building children, so a callout's body (and any self-drawing
9
+ * component nested inside it, like a `card`) is built exactly once, at the
10
+ * correct width, never built wide and re-wrapped narrower afterward — see
11
+ * `resolveCardInnerWidth`'s doc comment for why that re-wrap-afterward
12
+ * shape was a real, shipped regression.
13
+ */
14
+ export declare function resolveCalloutInnerWidth(attributes: DirectiveAttributes, layout: ResolvedLayoutPresets | undefined, outerWidth: number): number;
3
15
  /**
4
16
  * `:::callout{type=info|warning|danger title="..." text=left|center|right}` —
5
17
  * a colored aside/warning/danger box. Unknown/missing `type` falls back to
6
- * `info` (`render.ts`'s generic invalid-enum notice covers reporting that,
7
- * same as every other enum attribute). Terminal form: every line carries a
8
- * colored left bar; the first line is the icon plus the type label, an
9
- * optional bold title line follows, then the body wrapped to the remaining
10
- * width. `text` aligns the icon/title/body lines within that remaining
11
- * width; absent/invalid `text` behaves as `left` (no padding needed).
18
+ * `info`. Terminal form: every line carries a colored left bar; the first
19
+ * line is the icon plus the type label, an optional bold title line
20
+ * follows, then the body. `text` aligns the icon/title/body lines within
21
+ * the remaining width.
12
22
  *
13
- * Registered `selfLayout` (`registry.ts`'s `AnsiRegistryEntry.selfLayout`):
14
- * every line carries the colored bar prefix, which a generic post-render
15
- * `applyLayout` narrowing would re-wrap right through, losing the bar on any
16
- * continuation line. This component instead reads the resolved
17
- * `width`/`align` off `ctx.layout` itself and sizes/places its own bar
18
- * block, exactly like `card`/`table`/`chart`.
23
+ * Registered `selfLayout`: every line carries the colored bar prefix, which
24
+ * a generic post-render `applyLayout` narrowing would re-wrap right through,
25
+ * losing the bar on any continuation line. This component reads the
26
+ * resolved `width`/`align` off `ctx.layout` itself and sizes/places its own
27
+ * bar block, exactly like `card`/`table`/`divider`. `children` arrives
28
+ * ALREADY built at exactly this component's own inner width
29
+ * (`resolveCalloutInnerWidth` above) — this component never re-wraps.
19
30
  */
20
31
  export declare const Callout: AnsiComponent;
@@ -1,5 +1,6 @@
1
- import { measure } from '../measure.js';
1
+ import { measureWidth, padText } from '../text-grid.js';
2
2
  import { selfLayoutAlign, selfLayoutWidth } from '../layout.js';
3
+ import { childrenText, } from '../registry.js';
3
4
  const CALLOUT_TYPES = ['info', 'warning', 'danger'];
4
5
  const CALLOUT_ICONS = {
5
6
  info: 'ℹ',
@@ -23,47 +24,70 @@ const TEXT_ALIGNS = ['left', 'center', 'right'];
23
24
  function isTextAlign(value) {
24
25
  return TEXT_ALIGNS.includes(value);
25
26
  }
26
- /** The left bar every line of a callout carries, marking it as one colored block at a glance (`box.ts` reserves `│`/`┆` for frames, so this uses the half-block glyph instead). */
27
+ /** The left bar every line of a callout carries, marking it as one colored block at a glance. */
27
28
  const BAR = '▌ ';
29
+ /** `headerText` for `type`, e.g. `ℹ Info` — needed by both `resolveCalloutInnerWidth` (before there is a component instance) and `Callout` itself. */
30
+ function headerTextFor(type) {
31
+ return `${CALLOUT_ICONS[type]} ${CALLOUT_LABELS[type]}`;
32
+ }
33
+ /**
34
+ * The inner budget `callout`'s body renders at, given ONLY its own
35
+ * attributes and the outer width — never the body itself. Mirrors
36
+ * `card.ts`'s `resolveCardInnerWidth`: `render.tsx`'s walk calls this
37
+ * BEFORE building children, so a callout's body (and any self-drawing
38
+ * component nested inside it, like a `card`) is built exactly once, at the
39
+ * correct width, never built wide and re-wrapped narrower afterward — see
40
+ * `resolveCardInnerWidth`'s doc comment for why that re-wrap-afterward
41
+ * shape was a real, shipped regression.
42
+ */
43
+ export function resolveCalloutInnerWidth(attributes, layout, outerWidth) {
44
+ const rawType = attributes.type ?? 'info';
45
+ const type = isCalloutType(rawType) ? rawType : 'info';
46
+ const title = attributes.title ?? null;
47
+ const naturalWidth = BAR.length +
48
+ Math.max(measureWidth(headerTextFor(type)), measureWidth(title ?? ''));
49
+ const boxWidth = selfLayoutWidth(layout, outerWidth, naturalWidth);
50
+ return Math.max(1, boxWidth - BAR.length);
51
+ }
28
52
  /**
29
53
  * `:::callout{type=info|warning|danger title="..." text=left|center|right}` —
30
54
  * a colored aside/warning/danger box. Unknown/missing `type` falls back to
31
- * `info` (`render.ts`'s generic invalid-enum notice covers reporting that,
32
- * same as every other enum attribute). Terminal form: every line carries a
33
- * colored left bar; the first line is the icon plus the type label, an
34
- * optional bold title line follows, then the body wrapped to the remaining
35
- * width. `text` aligns the icon/title/body lines within that remaining
36
- * width; absent/invalid `text` behaves as `left` (no padding needed).
55
+ * `info`. Terminal form: every line carries a colored left bar; the first
56
+ * line is the icon plus the type label, an optional bold title line
57
+ * follows, then the body. `text` aligns the icon/title/body lines within
58
+ * the remaining width.
37
59
  *
38
- * Registered `selfLayout` (`registry.ts`'s `AnsiRegistryEntry.selfLayout`):
39
- * every line carries the colored bar prefix, which a generic post-render
40
- * `applyLayout` narrowing would re-wrap right through, losing the bar on any
41
- * continuation line. This component instead reads the resolved
42
- * `width`/`align` off `ctx.layout` itself and sizes/places its own bar
43
- * block, exactly like `card`/`table`/`chart`.
60
+ * Registered `selfLayout`: every line carries the colored bar prefix, which
61
+ * a generic post-render `applyLayout` narrowing would re-wrap right through,
62
+ * losing the bar on any continuation line. This component reads the
63
+ * resolved `width`/`align` off `ctx.layout` itself and sizes/places its own
64
+ * bar block, exactly like `card`/`table`/`divider`. `children` arrives
65
+ * ALREADY built at exactly this component's own inner width
66
+ * (`resolveCalloutInnerWidth` above) — this component never re-wraps.
44
67
  */
45
- export const Callout = (attributes, children, ctx) => {
68
+ export const Callout = ({ attributes, children, ctx }) => {
46
69
  const rawType = attributes.type ?? 'info';
47
70
  const type = isCalloutType(rawType) ? rawType : 'info';
48
71
  const token = CALLOUT_TOKENS[type];
49
72
  const title = attributes.title ?? null;
50
73
  const rawTextAlign = attributes.text;
51
74
  const align = rawTextAlign && isTextAlign(rawTextAlign) ? rawTextAlign : 'left';
52
- const headerText = `${CALLOUT_ICONS[type]} ${CALLOUT_LABELS[type]}`;
75
+ const headerText = headerTextFor(type);
53
76
  const titleText = title ? ctx.text(title) : undefined;
54
- const naturalWidth = BAR.length + Math.max(measure(headerText), measure(titleText ?? ''));
77
+ const naturalWidth = BAR.length +
78
+ Math.max(measureWidth(headerText), measureWidth(titleText ?? ''));
55
79
  const boxWidth = selfLayoutWidth(ctx.layout, ctx.width, naturalWidth);
56
80
  const bar = ctx.style(BAR, token);
57
81
  const innerWidth = Math.max(1, boxWidth - BAR.length);
58
- const placeLine = (line) => align === 'left' ? line : ctx.pad(line, innerWidth, align);
82
+ const placeLine = (line) => align === 'left' ? line : padText(line, innerWidth, align);
59
83
  const headerLine = `${bar}${placeLine(ctx.style(headerText, token))}`;
60
84
  const lines = [headerLine];
61
85
  if (titleText) {
62
86
  lines.push(`${bar}${placeLine(ctx.bold(titleText))}`);
63
87
  }
64
- const childrenText = children({ width: innerWidth });
65
- if (childrenText) {
66
- for (const line of childrenText.split('\n')) {
88
+ const body = childrenText(children);
89
+ if (body) {
90
+ for (const line of body.split('\n')) {
67
91
  lines.push(`${bar}${placeLine(line)}`);
68
92
  }
69
93
  }
@@ -1,4 +1,23 @@
1
- import type { AnsiComponent } from '../registry.js';
1
+ import type { ResolvedLayoutPresets } from '../layout.js';
2
+ import { type AnsiComponent, type DirectiveAttributes } from '../registry.js';
3
+ /**
4
+ * The inner budget `card`'s body renders at, given ONLY its own attributes
5
+ * and the outer width — never the body itself. This is deliberately a pure
6
+ * function callable BEFORE any children exist: `render.tsx`'s walk calls it
7
+ * to decide the width its OWN recursive children-builder uses, so `card`'s
8
+ * children are built ONCE, directly at the correct inner width, and never
9
+ * built wide and re-wrapped narrower afterward.
10
+ *
11
+ * That re-wrap-afterward shape was a real, shipped regression (caught
12
+ * post-phase-2 review): re-wrapping an ALREADY-RENDERED child block with a
13
+ * word-based wrapper corrupts anything in it that draws its own fixed-width
14
+ * glyphs (a nested `card`/`callout`'s frame, a nested `row`'s columns) — the
15
+ * word-wrapper has no notion that a run of box-drawing characters is not
16
+ * prose. Computing the inner width eagerly and building children exactly
17
+ * once at it is the fix, and it is what makes the deleted lazy-children API
18
+ * safe to have removed: this is its eager equivalent.
19
+ */
20
+ export declare function resolveCardInnerWidth(attributes: DirectiveAttributes, layout: ResolvedLayoutPresets | undefined, outerWidth: number): number;
2
21
  /**
3
22
  * `:::card{title="..." text=left|center|right} ... :::` — a titled panel.
4
23
  * `title` is optional; the title is woven into the frame's top edge, not a
@@ -7,6 +26,9 @@ import type { AnsiComponent } from '../registry.js';
7
26
  *
8
27
  * Registered `selfLayout` (see `callout.ts`'s doc comment for why): this
9
28
  * component draws a real box-drawn frame, which a generic post-render
10
- * narrow/pad would corrupt, so it reads `ctx.layout` and sizes its own frame.
29
+ * narrow/pad would corrupt, so it reads `ctx.layout` and sizes its own
30
+ * frame. `children` arrives ALREADY built at exactly this component's own
31
+ * inner width (`render.tsx`'s walk calls `resolveCardInnerWidth` above
32
+ * before building them) — this component never re-wraps.
11
33
  */
12
34
  export declare const Card: AnsiComponent;
@@ -1,13 +1,39 @@
1
- import { measure } from '../measure.js';
1
+ import { measureWidth, frameBlock, padText } from '../text-grid.js';
2
2
  import { selfLayoutAlign, selfLayoutWidth } from '../layout.js';
3
+ import { childrenText, } from '../registry.js';
3
4
  const TEXT_ALIGNS = ['left', 'center', 'right'];
4
5
  function isTextAlign(value) {
5
6
  return TEXT_ALIGNS.includes(value);
6
7
  }
7
- /** `width=fit` with no `title` and an unhelpfully-shaped body (nothing to measure a sensible box from) falls back to this column count rather than the full available width. */
8
+ /** `width=fit` with no `title` and an unhelpfully-shaped body falls back to this column count rather than the full available width. */
8
9
  const FIT_DEFAULT_WIDTH = 40;
9
10
  /** Frame border + one space of interior padding on each side. */
10
11
  const FRAME_OVERHEAD = 2;
12
+ /**
13
+ * The inner budget `card`'s body renders at, given ONLY its own attributes
14
+ * and the outer width — never the body itself. This is deliberately a pure
15
+ * function callable BEFORE any children exist: `render.tsx`'s walk calls it
16
+ * to decide the width its OWN recursive children-builder uses, so `card`'s
17
+ * children are built ONCE, directly at the correct inner width, and never
18
+ * built wide and re-wrapped narrower afterward.
19
+ *
20
+ * That re-wrap-afterward shape was a real, shipped regression (caught
21
+ * post-phase-2 review): re-wrapping an ALREADY-RENDERED child block with a
22
+ * word-based wrapper corrupts anything in it that draws its own fixed-width
23
+ * glyphs (a nested `card`/`callout`'s frame, a nested `row`'s columns) — the
24
+ * word-wrapper has no notion that a run of box-drawing characters is not
25
+ * prose. Computing the inner width eagerly and building children exactly
26
+ * once at it is the fix, and it is what makes the deleted lazy-children API
27
+ * safe to have removed: this is its eager equivalent.
28
+ */
29
+ export function resolveCardInnerWidth(attributes, layout, outerWidth) {
30
+ const title = attributes.title ?? null;
31
+ const naturalWidth = title
32
+ ? measureWidth(title) + FRAME_OVERHEAD * 2
33
+ : FIT_DEFAULT_WIDTH;
34
+ const boxWidth = selfLayoutWidth(layout, outerWidth, naturalWidth);
35
+ return Math.max(1, boxWidth - FRAME_OVERHEAD);
36
+ }
11
37
  /**
12
38
  * `:::card{title="..." text=left|center|right} ... :::` — a titled panel.
13
39
  * `title` is optional; the title is woven into the frame's top edge, not a
@@ -16,26 +42,29 @@ const FRAME_OVERHEAD = 2;
16
42
  *
17
43
  * Registered `selfLayout` (see `callout.ts`'s doc comment for why): this
18
44
  * component draws a real box-drawn frame, which a generic post-render
19
- * narrow/pad would corrupt, so it reads `ctx.layout` and sizes its own frame.
45
+ * narrow/pad would corrupt, so it reads `ctx.layout` and sizes its own
46
+ * frame. `children` arrives ALREADY built at exactly this component's own
47
+ * inner width (`render.tsx`'s walk calls `resolveCardInnerWidth` above
48
+ * before building them) — this component never re-wraps.
20
49
  */
21
- export const Card = (attributes, children, ctx) => {
50
+ export const Card = ({ attributes, children, ctx }) => {
22
51
  const title = attributes.title ?? null;
23
52
  const titleText = title ? ctx.text(title) : undefined;
24
53
  const rawTextAlign = attributes.text;
25
54
  const align = rawTextAlign && isTextAlign(rawTextAlign) ? rawTextAlign : 'left';
26
55
  const naturalWidth = titleText
27
- ? measure(titleText) + FRAME_OVERHEAD * 2
56
+ ? measureWidth(titleText) + FRAME_OVERHEAD * 2
28
57
  : FIT_DEFAULT_WIDTH;
29
58
  const boxWidth = selfLayoutWidth(ctx.layout, ctx.width, naturalWidth);
30
59
  const innerWidth = Math.max(1, boxWidth - FRAME_OVERHEAD);
31
- const childrenText = children({ width: innerWidth });
32
- const body = childrenText
33
- ? childrenText
60
+ const bodyText = childrenText(children);
61
+ const body = align === 'left'
62
+ ? bodyText
63
+ : bodyText
34
64
  .split('\n')
35
- .map((line) => align === 'left' ? line : ctx.pad(line, innerWidth, align))
36
- .join('\n')
37
- : '';
38
- const framed = ctx.frame(body, {
65
+ .map((line) => padText(line, innerWidth, align))
66
+ .join('\n');
67
+ const framed = frameBlock(body, {
39
68
  style: 'solid',
40
69
  title: titleText,
41
70
  width: boxWidth,
@@ -1,18 +1,12 @@
1
- import type { AnsiComponent } from '../registry.js';
1
+ import { type AnsiComponent } from '../registry.js';
2
2
  /**
3
3
  * `:::cell ... :::` — a transparent grouping container whose only job is
4
4
  * letting several blocks count as ONE cell of `:::row`.
5
5
  *
6
- * `row.ts` no longer discovers its cells by splitting a flattened string on
7
- * a blank-line heuristic: it walks its own directive's top-level children
8
- * directly (`registry.ts`'s `AnsiChildren.parts`), so a `cell` grouping more
9
- * than one block renders those blocks exactly as it would standalone,
10
- * blank line and all, with no collapsing needed to keep a cell boundary
11
- * unambiguous for `row.ts`.
12
- *
13
6
  * `text` aligns this cell's own content when rendered STANDALONE (outside a
14
- * `row`); nested inside a `row`, the row's own `text` decides every cell's
15
- * alignment uniformly instead, for the same reason `row.ts` does not look
16
- * inside a cell's own attributes when it places columns.
7
+ * `row`) or nested inside one: `render.tsx`'s `row` handling still runs
8
+ * each cell through the ordinary directive pipeline (so this component's
9
+ * own alignment applies first), then applies `row`'s OWN `text` uniformly
10
+ * on top — see `components/row.ts`'s doc comment.
17
11
  */
18
12
  export declare const Cell: AnsiComponent;
@@ -1,3 +1,5 @@
1
+ import { padText } from '../text-grid.js';
2
+ import { childrenText } from '../registry.js';
1
3
  const TEXT_ALIGNS = ['left', 'center', 'right'];
2
4
  function isTextAlign(value) {
3
5
  return TEXT_ALIGNS.includes(value);
@@ -6,26 +8,20 @@ function isTextAlign(value) {
6
8
  * `:::cell ... :::` — a transparent grouping container whose only job is
7
9
  * letting several blocks count as ONE cell of `:::row`.
8
10
  *
9
- * `row.ts` no longer discovers its cells by splitting a flattened string on
10
- * a blank-line heuristic: it walks its own directive's top-level children
11
- * directly (`registry.ts`'s `AnsiChildren.parts`), so a `cell` grouping more
12
- * than one block renders those blocks exactly as it would standalone,
13
- * blank line and all, with no collapsing needed to keep a cell boundary
14
- * unambiguous for `row.ts`.
15
- *
16
11
  * `text` aligns this cell's own content when rendered STANDALONE (outside a
17
- * `row`); nested inside a `row`, the row's own `text` decides every cell's
18
- * alignment uniformly instead, for the same reason `row.ts` does not look
19
- * inside a cell's own attributes when it places columns.
12
+ * `row`) or nested inside one: `render.tsx`'s `row` handling still runs
13
+ * each cell through the ordinary directive pipeline (so this component's
14
+ * own alignment applies first), then applies `row`'s OWN `text` uniformly
15
+ * on top — see `components/row.ts`'s doc comment.
20
16
  */
21
- export const Cell = (attributes, children, ctx) => {
17
+ export const Cell = ({ attributes, children, ctx }) => {
22
18
  const rawTextAlign = attributes.text;
23
19
  const align = rawTextAlign && isTextAlign(rawTextAlign) ? rawTextAlign : undefined;
24
- const childrenText = children();
20
+ const text = childrenText(children);
25
21
  if (!align || align === 'left')
26
- return childrenText;
27
- return childrenText
22
+ return text;
23
+ return text
28
24
  .split('\n')
29
- .map((line) => ctx.pad(line, ctx.width, align))
25
+ .map((line) => padText(line, ctx.width, align))
30
26
  .join('\n');
31
27
  };
@@ -1,15 +1,9 @@
1
1
  import type { AnsiComponent } from '../registry.js';
2
2
  /**
3
3
  * `::chart{kind=line|bar values="1,3,2,5"}` — a dependency-free, hand-rolled
4
- * chart. Data binding (§8) mirrors `@markii/html`'s `Chart`: a bound `data`
5
- * array of numbers (or `{value}` objects) takes priority over the static
6
- * `values=` attribute; non-numeric/non-finite entries are dropped and the
7
- * point count capped at `MAX_POINTS`. An empty or all-invalid series renders
8
- * a small neutral "no data" line rather than a broken chart.
9
- *
10
- * Terminal form: a `line` chart is a single sparkline row (`▁▂▃▄▅▆▇█`),
11
- * flanked by its minimum and maximum as plain numbers. A `bar` chart is one
12
- * horizontal `█` bar per point, its own value as the row's label, labels
13
- * right-aligned to the widest one so every bar starts at the same column.
4
+ * chart. Data binding (§8): a bound `data` array of numbers (or `{value}`
5
+ * objects) takes priority over the static `values=` attribute. Terminal
6
+ * form: a `line` chart is a single sparkline row, a `bar` chart is one
7
+ * horizontal `█` bar per point.
14
8
  */
15
9
  export declare const Chart: AnsiComponent;
@@ -1,5 +1,4 @@
1
- import { pad } from '../box.js';
2
- import { measure } from '../measure.js';
1
+ import { padText, measureWidth } from '../text-grid.js';
3
2
  import { safeRead } from '../resolve.js';
4
3
  import { dataStateSuffix, failureToken } from '../failure-presentation.js';
5
4
  const CHART_KINDS = ['line', 'bar'];
@@ -30,7 +29,7 @@ function coercePoint(entry) {
30
29
  }
31
30
  return undefined;
32
31
  }
33
- /** Builds the numeric point series to plot, a bound `data` array taking priority over the static `values=` attribute. Ported from `@markii/html`'s `resolvePoints`. */
32
+ /** Builds the numeric point series to plot, a bound `data` array taking priority over the static `values=` attribute. */
34
33
  function resolvePoints(data, dataStatus, rawValues) {
35
34
  if (dataStatus !== 'missing' &&
36
35
  dataStatus !== 'error' &&
@@ -85,18 +84,12 @@ function barLength(point, maxMagnitude, budget) {
85
84
  }
86
85
  /**
87
86
  * `::chart{kind=line|bar values="1,3,2,5"}` — a dependency-free, hand-rolled
88
- * chart. Data binding (§8) mirrors `@markii/html`'s `Chart`: a bound `data`
89
- * array of numbers (or `{value}` objects) takes priority over the static
90
- * `values=` attribute; non-numeric/non-finite entries are dropped and the
91
- * point count capped at `MAX_POINTS`. An empty or all-invalid series renders
92
- * a small neutral "no data" line rather than a broken chart.
93
- *
94
- * Terminal form: a `line` chart is a single sparkline row (`▁▂▃▄▅▆▇█`),
95
- * flanked by its minimum and maximum as plain numbers. A `bar` chart is one
96
- * horizontal `█` bar per point, its own value as the row's label, labels
97
- * right-aligned to the widest one so every bar starts at the same column.
87
+ * chart. Data binding (§8): a bound `data` array of numbers (or `{value}`
88
+ * objects) takes priority over the static `values=` attribute. Terminal
89
+ * form: a `line` chart is a single sparkline row, a `bar` chart is one
90
+ * horizontal `█` bar per point.
98
91
  */
99
- export const Chart = (attributes, _children, ctx) => {
92
+ export const Chart = ({ attributes, ctx }) => {
100
93
  const { data, dataStatus, dataFailureKind } = ctx;
101
94
  const rawKind = attributes.kind ?? DEFAULT_KIND;
102
95
  const kind = isChartKind(rawKind) ? rawKind : DEFAULT_KIND;
@@ -118,12 +111,12 @@ export const Chart = (attributes, _children, ctx) => {
118
111
  return `${min} ${sparkline(points)} ${max}${styledSuffix}`;
119
112
  }
120
113
  const labels = points.map((point) => formatPoint(point));
121
- const labelWidth = Math.max(...labels.map((label) => measure(label)));
114
+ const labelWidth = Math.max(...labels.map((label) => measureWidth(label)));
122
115
  const budget = Math.max(1, ctx.width - labelWidth - 1);
123
116
  const maxMagnitude = Math.max(...points.map((point) => Math.abs(point)));
124
117
  const lines = points.map((point, index) => {
125
118
  const bar = '█'.repeat(barLength(point, maxMagnitude, budget));
126
- return `${pad(labels[index], labelWidth, 'right')} ${bar}`;
119
+ return `${padText(labels[index], labelWidth, 'right')} ${bar}`;
127
120
  });
128
121
  return `${lines.join('\n')}${styledSuffix}`;
129
122
  };
@@ -0,0 +1,26 @@
1
+ import { type AnsiComponent } from '../registry.js';
2
+ /** Columns the indent prefix (" ") consumes from the enclosing width. */
3
+ export declare const DETAILS_INDENT_WIDTH = 2;
4
+ /**
5
+ * The inner budget `details`'s body renders at, given only the outer width
6
+ * — never the body itself. Mirrors `card.ts`'s `resolveCardInnerWidth`:
7
+ * `render.tsx`'s walk calls this BEFORE building children (for both the
8
+ * STRING-mode fallback below and the real interactive element builder in
9
+ * `details.tsx`), so the body — and anything self-drawing nested inside it
10
+ * — is built exactly once, at the correct width, never re-wrapped
11
+ * afterward.
12
+ */
13
+ export declare function resolveDetailsInnerWidth(outerWidth: number): number;
14
+ /**
15
+ * The STRING-mode `details` registration (used by `render.tsx`'s registry
16
+ * lookups, and as the fallback rendering when a `details` directive is
17
+ * reached through STRING mode — nested inside a self-drawing container's
18
+ * own body, see `render.tsx`'s top comment). The real Ink/interactive
19
+ * version `render.tsx` builds directly for a `details` reached through
20
+ * ELEMENT mode is `InteractiveDetails` in `./details.tsx`; the two share no
21
+ * code because this one can only ever show the body (a plain string has no
22
+ * expand/collapse affordance), exactly like the pre-Ink engine. `children`
23
+ * arrives ALREADY built at exactly `resolveDetailsInnerWidth`'s width —
24
+ * this component never re-wraps.
25
+ */
26
+ export declare const Details: AnsiComponent;
@@ -0,0 +1,39 @@
1
+ import { indentBlock } from '../text-grid.js';
2
+ import { childrenText } from '../registry.js';
3
+ const DEFAULT_TITLE = 'Details';
4
+ /** Columns the indent prefix (" ") consumes from the enclosing width. */
5
+ export const DETAILS_INDENT_WIDTH = 2;
6
+ /**
7
+ * The inner budget `details`'s body renders at, given only the outer width
8
+ * — never the body itself. Mirrors `card.ts`'s `resolveCardInnerWidth`:
9
+ * `render.tsx`'s walk calls this BEFORE building children (for both the
10
+ * STRING-mode fallback below and the real interactive element builder in
11
+ * `details.tsx`), so the body — and anything self-drawing nested inside it
12
+ * — is built exactly once, at the correct width, never re-wrapped
13
+ * afterward.
14
+ */
15
+ export function resolveDetailsInnerWidth(outerWidth) {
16
+ return Math.max(1, outerWidth - DETAILS_INDENT_WIDTH);
17
+ }
18
+ /**
19
+ * The STRING-mode `details` registration (used by `render.tsx`'s registry
20
+ * lookups, and as the fallback rendering when a `details` directive is
21
+ * reached through STRING mode — nested inside a self-drawing container's
22
+ * own body, see `render.tsx`'s top comment). The real Ink/interactive
23
+ * version `render.tsx` builds directly for a `details` reached through
24
+ * ELEMENT mode is `InteractiveDetails` in `./details.tsx`; the two share no
25
+ * code because this one can only ever show the body (a plain string has no
26
+ * expand/collapse affordance), exactly like the pre-Ink engine. `children`
27
+ * arrives ALREADY built at exactly `resolveDetailsInnerWidth`'s width —
28
+ * this component never re-wraps.
29
+ */
30
+ export const Details = ({ attributes, children, ctx }) => {
31
+ const title = attributes.title ?? DEFAULT_TITLE;
32
+ const open = Object.hasOwn(attributes, 'open');
33
+ const glyph = open ? '▾' : '▸';
34
+ const marker = ctx.dim(`${glyph} ${ctx.bold(ctx.text(title))} (collapsible section, shown expanded)`);
35
+ const body = childrenText(children);
36
+ if (!body)
37
+ return marker;
38
+ return `${marker}\n${indentBlock(body, ' ')}`;
39
+ };